summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--func.spec6
-rw-r--r--func/minion/modules/certmastermod.py (renamed from func/minion/modules/certmaster.py)10
-rw-r--r--func/minion/modules/delegation.py35
-rw-r--r--func/minion/modules/overlord.py41
-rw-r--r--func/overlord/base_command.py8
-rwxr-xr-xfunc/overlord/client.py103
-rw-r--r--func/overlord/cmd_modules/call.py6
-rw-r--r--func/overlord/delegation_tools.py155
-rwxr-xr-xfunc/overlord/mapper.py91
-rwxr-xr-xscripts/func-build-map8
-rw-r--r--setup.py2
-rw-r--r--version2
12 files changed, 449 insertions, 18 deletions
diff --git a/func.spec b/func.spec
index b4b1410..bf6d3df 100644
--- a/func.spec
+++ b/func.spec
@@ -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)
diff --git a/setup.py b/setup.py
index 992992a..0c79130 100644
--- a/setup.py
+++ b/setup.py
@@ -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,
diff --git a/version b/version
index c9ec35a..3ecda66 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.22 1
+0.23 1