diff options
-rw-r--r-- | func.spec | 6 | ||||
-rw-r--r-- | func/minion/modules/certmastermod.py (renamed from func/minion/modules/certmaster.py) | 10 | ||||
-rw-r--r-- | func/minion/modules/delegation.py | 35 | ||||
-rw-r--r-- | func/minion/modules/overlord.py | 41 | ||||
-rw-r--r-- | func/overlord/base_command.py | 8 | ||||
-rwxr-xr-x | func/overlord/client.py | 103 | ||||
-rw-r--r-- | func/overlord/cmd_modules/call.py | 6 | ||||
-rw-r--r-- | func/overlord/delegation_tools.py | 155 | ||||
-rwxr-xr-x | func/overlord/mapper.py | 91 | ||||
-rwxr-xr-x | scripts/func-build-map | 8 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | version | 2 |
12 files changed, 449 insertions, 18 deletions
@@ -13,6 +13,7 @@ License: GPLv2+ Group: Applications/System Requires: python >= 2.3 Requires: pyOpenSSL +Requires: pyYaml Requires: certmaster >= 0.1 BuildRequires: python-devel %if %is_suse @@ -55,6 +56,7 @@ rm -fr $RPM_BUILD_ROOT %{_bindir}/func-inventory %{_bindir}/func-create-module %{_bindir}/func-transmit +%{_bindir}/func-build-map #%{_bindir}/update-func /etc/init.d/funcd %dir %{_sysconfdir}/%{name} @@ -128,6 +130,9 @@ fi %changelog +* Fri Jul 11 2008 Michael DeHaan <mdehaan@redhat.com> - 0.23-1 +- (for ssalevan) adding in mapping/delegation tools + * Mon Jul 07 2008 Michael DeHaan <mdehaan@redhat.com> - 0.22-1 - packaged func-transmit script @@ -141,6 +146,7 @@ fi - fix fedora bug #441283 - typo in postinstall scriptlet (the init.d symlinks for runlevels 1 and 6 were created wrong) + * Mon Mar 03 2008 Adrian Likins <alikins@redhat.com> - 0.18-1 - split off certmaster diff --git a/func/minion/modules/certmaster.py b/func/minion/modules/certmastermod.py index 7237ffa..73f1468 100644 --- a/func/minion/modules/certmaster.py +++ b/func/minion/modules/certmastermod.py @@ -15,7 +15,7 @@ # our modules import func_module -import certmaster.certmaster as certmaster +from certmaster import certmaster as certmaster # ================================= @@ -32,6 +32,14 @@ class CertMasterModule(func_module.FuncModule): list_of_hosts = self.__listify(list_of_hosts) cm = certmaster.CertMaster() return cm.get_csrs_waiting() + + def get_signed_certs(self, list_of_hosts): + """ + Returns a list of all signed certs on this minion + """ + list_of_hosts = self.__listify(list_of_hosts) + cm = certmaster.CertMaster() + return cm.get_signed_certs() def sign_hosts(self, list_of_hosts): """ diff --git a/func/minion/modules/delegation.py b/func/minion/modules/delegation.py new file mode 100644 index 0000000..9f34d1a --- /dev/null +++ b/func/minion/modules/delegation.py @@ -0,0 +1,35 @@ +# Copyright 2008, Red Hat, Inc +# Steve Salevan <ssalevan@redhat.com> +# +# This software may be freely redistributed under the terms of the GNU +# general public license. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import func_module +import func.overlord.client as fc +from func import utils + +class DelegationModule(func_module.FuncModule): + + version = "0.0.1" + api_version = "0.0.1" + description = "Minion-side module to support delegation on sub-Overlords." + + def run(self,module,method,args,delegation_path): + """ + Delegates commands down the path of delegation + supplied as an argument + """ + + next_hop = delegation_path[0] + overlord = fc.Overlord(next_hop) + if len(delegation_path) == 1: #minion exists under this overlord + overlord_module = getattr(overlord,module) + return getattr(overlord_module,method)(*args[:]) + + stripped_list = delegation_path[1:len(delegation_path)] + delegation_results = overlord.delegation.run(module,method,args,stripped_list) + return delegation_results[next_hop] #strip away nested hash data from results diff --git a/func/minion/modules/overlord.py b/func/minion/modules/overlord.py new file mode 100644 index 0000000..710f6d1 --- /dev/null +++ b/func/minion/modules/overlord.py @@ -0,0 +1,41 @@ +# Copyright 2008, Red Hat, Inc +# Steve Salevan <ssalevan@redhat.com> +# +# This software may be freely redistributed under the terms of the GNU +# general public license. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import func_module +import func.overlord.client as fc +from certmaster import certmaster as certmaster +from func import utils + +class OverlordModule(func_module.FuncModule): + + version = "0.0.1" + api_version = "0.0.1" + description = "Module for controlling minions that are also overlords." + + def map_minions(self,get_only_alive=False): + """ + Builds a recursive map of the minions currently assigned to this + overlord + """ + maphash = {} + current_minions = [] + if get_only_alive: + ping_results = fc.Overlord("*").test.ping() + for minion in ping_results.keys(): + if ping_results[minion] == 1: #if minion is alive + current_minions.append(minion) #add it to the list of current minions + else: + cm = certmaster.CertMaster() + current_minions = cm.get_signed_certs() + for current_minion in current_minions: + maphash[current_minion] = fc.Overlord(current_minion).overlord.map_minions()[current_minion] + return maphash + + diff --git a/func/overlord/base_command.py b/func/overlord/base_command.py index f7c33c0..9052c20 100644 --- a/func/overlord/base_command.py +++ b/func/overlord/base_command.py @@ -16,6 +16,8 @@ import command import client DEFAULT_PORT = 51234 +DEFAULT_MAPLOC = "/var/lib/func/map" + class BaseCommand(command.Command): """ wrapper class for commands with some convience functions, namely getOverlord() for getting a overlord client api handle""" @@ -25,6 +27,8 @@ class BaseCommand(command.Command): port=DEFAULT_PORT async=False forks=1 + delegate=False + mapfile=DEFAULT_MAPLOC def getOverlord(self): self.overlord_obj = client.Overlord(self.server_spec, port=self.port, @@ -32,4 +36,6 @@ class BaseCommand(command.Command): verbose=self.verbose, config=self.config, async=self.async, - nforks=self.forks) + nforks=self.forks, + delegate=self.delegate, + mapfile=self.mapfile) diff --git a/func/overlord/client.py b/func/overlord/client.py index c46cc1f..01a31a7 100755 --- a/func/overlord/client.py +++ b/func/overlord/client.py @@ -16,6 +16,7 @@ import sys import glob import os +import yaml from certmaster.commonconfig import CMConfig from func.config import read_config, CONFIG_FILE @@ -24,6 +25,7 @@ import sslclient import command import groups +import delegation_tools as dtools import func.forkbomb as forkbomb import func.jobthing as jobthing import func.utils as utils @@ -35,6 +37,8 @@ from func.CommonErrors import * DEFAULT_PORT = 51234 FUNC_USAGE = "Usage: %s [ --help ] [ --verbose ] target.example.org module method arg1 [...]" +DEFAULT_MAPLOC = "/var/lib/func/map" +DELEGATION_METH = "delegation.run" # =================================== @@ -88,13 +92,16 @@ class CommandAutomagic(object): class Minions(object): def __init__(self, spec, port=51234, noglobs=None, verbose=None, - just_fqdns=False, groups_file=None): + just_fqdns=False, groups_file=None, + delegate=False, minionmap={}): self.spec = spec self.port = port self.noglobs = noglobs self.verbose = verbose self.just_fqdns = just_fqdns + self.delegate = delegate + self.minionmap = minionmap self.config = read_config(CONFIG_FILE, CMConfig) self.group_class = groups.Groups(filename=groups_file) @@ -153,12 +160,11 @@ def is_minion(minion_string): return minions.is_minion() - - class Overlord(object): def __init__(self, server_spec, port=DEFAULT_PORT, interactive=False, - verbose=False, noglobs=False, nforks=1, config=None, async=False, init_ssl=True): + verbose=False, noglobs=False, nforks=1, config=None, async=False, init_ssl=True, + delegate=False, mapfile=DEFAULT_MAPLOC): """ Constructor. @server_spec -- something like "*.example.org" or "foosball" @@ -179,9 +185,19 @@ class Overlord(object): self.noglobs = noglobs self.nforks = nforks self.async = async + self.delegate = delegate + self.mapfile = mapfile self.minions_class = Minions(self.server_spec, port=self.port, noglobs=self.noglobs,verbose=self.verbose) self.minions = self.minions_class.get_urls() + + if self.delegate: + try: + mapstream = file(self.mapfile, 'r') + self.minionmap = yaml.load(mapstream) + except e: + sys.stderr.write("mapfile load failed, switching delegation off") + self.delegate = False if init_ssl: self.setup_ssl() @@ -256,9 +272,50 @@ class Overlord(object): If Overlord() was constructed with noglobs=True, the return is instead just a single value, not a hash. """ + + if not self.delegate: #delegation is turned off, so run normally + return self.run_direct(module, method, args, nforks) + + resulthash = {} + + #First we get all call paths for minions not directly beneath this overlord + dele_paths = dtools.get_paths_for_glob(self.server_spec, self.minionmap) + non_single_paths = [path for path in dele_paths if len(path) > 1] + + for path in non_single_paths: + resulthash.update(self.run_direct(module, + method, + args, + nforks, + call_path=path)) + + #Next, we run everything that can be run directly beneath this overlord + #Why do we do this after delegation calls? Imagine what happens when + #reboot is called... + resulthash.update(self.run_direct(module,method,args,nforks)) + + return resulthash + + + # ----------------------------------------------- - results = {} + def run_direct(self, module, method, args, nforks=1, *extraargs, **kwargs): + """ + Invoke a remote method on one or more servers. + Run returns a hash, the keys are server names, the values are the + returns. + + The returns may include exception objects. + If Overlord() was constructed with noglobs=True, the return is instead + just a single value, not a hash. + """ + results = {} + spec = '' + minionurls = [] + use_delegate = False + delegation_path = [] + def process_server(bucketnumber, buckets, server): conn = sslclient.FuncServer(server, self.key, self.cert, self.ca ) @@ -275,7 +332,10 @@ class Overlord(object): # thats some pretty code right there aint it? -akl # we can't call "call" on s, since thats a rpc, so # we call gettatr around it. - meth = "%s.%s" % (module, method) + if use_delegate: + meth = DELEGATION_METH #call delegation module + else: + meth = "%s.%s" % (module, method) # async calling signature has an "imaginary" prefix # so async.abc.def does abc.def as a background task. @@ -284,7 +344,10 @@ class Overlord(object): meth = "async.%s" % meth # this is the point at which we make the remote call. - retval = getattr(conn, meth)(*args[:]) + if use_delegate: + retval = getattr(conn, meth)(module, method, args, delegation_path) + else: + retval = getattr(conn, meth)(*args[:]) if self.interactive: print retval @@ -302,29 +365,43 @@ class Overlord(object): right = server.rfind(":") server_name = server[left:right] return (server_name, retval) - + + if kwargs.has_key('call_path'): #we're delegating if this key exists + spec = kwargs['call_path'][0] #the sub-overlord directly beneath this one + minionobj = Minions(spec, port=self.port, verbose=self.verbose) + use_delegate = True #signal to process_server to call delegate method + delegation_path = kwargs['call_path'][1:len(kwargs['call_path'])] + minionurls = minionobj.get_urls() #the single-item url list to make async + #tools such as jobthing/forkbomb happy + else: #we're directly calling minions, so treat everything normally + spec = self.server_spec + minionurls = self.minions + if not self.noglobs: if self.nforks > 1 or self.async: # using forkbomb module to distribute job over multiple threads if not self.async: - results = forkbomb.batch_run(self.minions, process_server, nforks) + results = forkbomb.batch_run(minionurls, process_server, nforks) else: - results = jobthing.batch_run(self.minions, process_server, nforks) + results = jobthing.batch_run(minionurls, process_server, nforks) else: # no need to go through the fork code, we can do this directly results = {} - for x in self.minions: + for x in minionurls: (nkey,nvalue) = process_server(0, 0, x) results[nkey] = nvalue else: # globbing is not being used, but still need to make sure # URI is well formed. # expanded = expand_servers(self.server_spec, port=self.port, noglobs=True, verbose=self.verbose)[0] - expanded_minions = Minions(self.server_spec, port=self.port, noglobs=True, verbose=self.verbose) + expanded_minions = Minions(spec, port=self.port, noglobs=True, verbose=self.verbose) minions = expanded_minions.get_urls()[0] # print minions results = process_server(0, 0, minions) - + + if use_delegate: + return results[spec] + return results # ----------------------------------------------- diff --git a/func/overlord/cmd_modules/call.py b/func/overlord/cmd_modules/call.py index e2989f1..e1871af 100644 --- a/func/overlord/cmd_modules/call.py +++ b/func/overlord/cmd_modules/call.py @@ -65,6 +65,9 @@ class Call(base_command.BaseCommand): self.parser.add_option("-s", "--jobstatus", dest="jobstatus", help="Do not run any job, just check for status.", action="store_true") + self.parser.add_option('-d', '--delegate', dest="delegate", + help="use delegation to make function call", + action="store_true") def handleOptions(self, options): self.options = options @@ -139,10 +142,11 @@ class Call(base_command.BaseCommand): self.interactive = False self.async = self.options.async self.forks = self.options.forks + self.delegate = self.options.delegate self.server_spec = self.parentCommand.server_spec self.getOverlord() - + print self.overlord_obj if not self.options.jobstatus: results = self.overlord_obj.run(self.module, self.method, self.method_args) diff --git a/func/overlord/delegation_tools.py b/func/overlord/delegation_tools.py new file mode 100644 index 0000000..f3445c0 --- /dev/null +++ b/func/overlord/delegation_tools.py @@ -0,0 +1,155 @@ +## +## func delegation tools +## These are some helper methods to make dealing with delegation +## dictionary trees a little more sane when dealing with delegation +## and related functions. +## +## Copyright 2008, Red Hat, Inc. +## Steve Salevan <ssalevan@redhat.com> +## +## This software may be freely redistributed under the terms of the GNU +## general public license. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +## + +import fnmatch + +def get_paths_for_glob(glob, minionmap): + """ + Given a glob, returns shortest path to all minions + matching it in the delegation dictionary tree + """ + + pathlist = [] + for elem in match_glob_in_tree(glob,minionmap): + result = get_shortest_path(elem,minionmap) + if result not in pathlist: #prevents duplicates + pathlist.append(result) + return pathlist + +def flatten_list(bumpy_list): + """ + Flattens gnarly nested lists into much + nicer, flat lists + """ + + flat_list = [] + for item in bumpy_list: + if isinstance(item, list): + for elem in flatten_list(item): + flat_list.append(elem) + else: + flat_list.append(item) + return flat_list + +def match_glob_on_toplevel(pattern, minionmap): + """ + Searches through the top level of a dictionary + for all keys (minion FQDNs) matching the given + glob, returns matches + """ + + matched = [] + for k,v in minionmap.iteritems(): + if fnmatch.fnmatch(k,pattern): + matched.append(k) + return matched + +def match_glob_in_tree(pattern, minionmap): + """ + Searches through given tree dictionary for all + keys (minion FQDNs) matching the given glob, + returns matches + """ + + matched = [] + for k,v in minionmap.iteritems(): + for result in match_glob_in_tree(pattern, v): + matched.append(result) + if fnmatch.fnmatch(k,pattern): + matched.append(k) + return matched + +def minion_exists_under_node(minion, minionmap): + """ + A little wrapper around the match_glob_on_toplevel + method that you can use if you want to get a boolean + result denoting minion existence under your current + node + """ + + return len(match_glob_on_toplevel(minion,minionmap)) > 0 + +def get_shortest_path(minion, minionmap): + """ + Given a minion that exists in the given tree, + this method returns all paths from the top + node to the minion in the form of a flat list + """ + + def lensort(a,b): + if len(a) > len(b): + return 1 + return -1 + + results = get_all_paths(minion,minionmap) + results.sort(lensort) + return results[0] + +def get_all_paths(minion, minionmap): + """ + Given a minion that exists in the given tree, + this method returns all paths that exist from the top + node to the minion in the delegation dictionary tree + """ + + #This is an ugly kludge of franken-code. If someone with + #more knowledge of graph theory than myself can improve this + #module, please, please do so. - ssalevan 7/2/08 + seq_list = [] + + if minion_exists_under_node(minion, minionmap): + return [[minion]] #minion found, terminate branch + + if minionmap == {}: + return [[]] #no minion found, terminate branch + + for k,v in minionmap.iteritems(): + branch_list = [] + branch_list.append(k) + + for branchlet in get_all_paths(minion, v): + branch_list.append(branchlet) + + single_branch = flatten_list(branch_list) + if minion in single_branch: + seq_list.append(single_branch) + + return seq_list + +if __name__ == "__main__": + mymap = {'anthony':{'longpath1':{'longpath2':{'longpath3':{}}}}, + 'phil':{'steve':{'longpath3':{}}}, + 'tony':{'mike':{'anthony':{}}}, + 'just_a_minion':{} + } + + print "- Testing an element that exists in multiple lists of varying length:" + for elem in match_glob_in_tree('*path3',mymap): + print "Element: %s, all paths: %s" % (elem, get_all_paths(elem,mymap)) + print "best path: %s" % get_shortest_path(elem, mymap) + + print "- Testing an element that is simply a minion and has no sub-nodes:" + for elem in match_glob_in_tree('*minion',mymap): + print "Element: %s, best path: %s" % (elem, get_shortest_path(elem,mymap)) + + print "- OK, now the whole thing:" + for elem in match_glob_in_tree('*',mymap): + print "Element: %s, best path: %s" % (elem, get_shortest_path(elem,mymap)) + + print "- And finally, with all duplicates removed:" + for elem in get_paths_for_glob('*path*',mymap): + print "Valid Path: %s" % elem diff --git a/func/overlord/mapper.py b/func/overlord/mapper.py new file mode 100755 index 0000000..345eb6b --- /dev/null +++ b/func/overlord/mapper.py @@ -0,0 +1,91 @@ +## +## func topology map-building tool +## If you've got a giant, tangled, complex web of func overlords +## and minions, this tool will help you construct or augment a map +## of your func network topology so that delegating commands to +## minions and overlords becomes a simple matter. +## +## Copyright 2008, Red Hat, Inc. +## Steve Salevan <ssalevan@redhat.com> +## +## This software may be freely redistributed under the terms of the GNU +## general public license. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +## + +import optparse +import sys +import yaml +import func.overlord.client as func_client + +DEFAULT_TREE = "/var/lib/func/map" + +class MapperTool(object): + + def __init__(self): + pass + + def run(self,args): + + p = optparse.OptionParser() + #currently not implemented + p.add_option("-a", "--append", + dest="append", + action="store_true", + help="append new map to current map") + p.add_option("-o", "--onlyalive", + dest="only_alive", + action="store_true", + help="gather only currently-living minions") + p.add_option("-v", "--verbose", + dest="verbose", + action="store_true", + help="provide extra output") + + (options, args) = p.parse_args(args) + self.options = options + + if options.verbose: + print "- recursively calling map function" + + self.build_map() + + return 1 + + def build_map(self): + + minion_hash = func_client.Overlord("*").overlord.map_minions(self.options.only_alive==True) + + if self.options.verbose: + print "- built the following map:" + print minion_hash + + if self.options.append: + try: + oldmap = file(DEFAULT_TREE, 'r') + old_hash = yaml.load(oldmap) + oldmap.close() + except e: + print "ERROR: old map could not be read, append failed" + sys.exit(-1) + + merged_map = {} + merged_map.update(old_hash) + merged_map.update(minion_hash) + + if self.options.verbose: + print "- appended new map to the following map:" + print old_hash + print " resulting in:" + print merged_map + + minion_hash = merged_map + + if self.options.verbose: + print "- writing to %s" % DEFAULT_TREE + + mapfile = file(DEFAULT_TREE, 'w') + yaml.dump(minion_hash,mapfile) diff --git a/scripts/func-build-map b/scripts/func-build-map new file mode 100755 index 0000000..734929a --- /dev/null +++ b/scripts/func-build-map @@ -0,0 +1,8 @@ +#!/usr/bin/python + +import sys +import distutils.sysconfig +import func.overlord.mapper as mapper + +mapper = mapper.MapperTool() +mapper.run(sys.argv) @@ -34,8 +34,8 @@ if __name__ == "__main__": "scripts/func-inventory", "scripts/func-create-module", "scripts/func-transmit" + "scripts/func-build-map" ], - # package_data = { '' : ['*.*'] }, package_dir = {"%s" % NAME: "%s" % NAME }, packages = ["%s" % NAME, @@ -1 +1 @@ -0.22 1 +0.23 1 |