---
snake-machine | 392 ++++++++++++++++++++++++++++++++++++++++++++++++++
snake-server | 34 ++++--
snake.spec | 4 +-
snake/config.py | 2 +-
snake/machine.py | 160 +++++++++++++--------
snake/machinedb.py | 209 +++++++++++++++++++++++++++
snake/machineinfo.py | 4 +-
snake/tui.py | 44 +++++-
snake/util.py | 19 +++
9 files changed, 789 insertions(+), 79 deletions(-)
create mode 100644 snake-machine
create mode 100644 snake/machinedb.py
diff --git a/snake-machine b/snake-machine
new file mode 100644
index 0000000..661a40f
--- /dev/null
+++ b/snake-machine
@@ -0,0 +1,392 @@
+#!/usr/bin/python
+# snake-machine - manage the machine database
+
+import os,sys
+import optparse
+import xmlrpclib
+import snake.client, snake.log, snake.machine, snake.machineinfo
+from socket import gethostname
+import rpmUtils.arch
+
+try:
+ import snake.machinedb
+ import snake.config
+except ImportError, e:
+ pass
+
+client_commands = ["add", "register", "update",
"list", "whoami", "info", "remove"]
+
+def print_table(machines, action=None, sort=True):
+ '''Display a formatted table of machines.
+ Accepts arguments:
+ - a list of machine objects or machine dictionaries
+ - optional string representing an action
+ - optional boolean indicating whether sorting is needed
+ '''
+
+ # sort the list for display
+ # FIXME: should this be done by the server instead?
+ if sort:
+ undecorated = [isinstance(t,dict) and t or t.__dict__ for t in machines]
+ decorated = [(int(dict_['id']), dict_) for dict_ in undecorated]
+ decorated.sort()
+ machines = [dict_ for (key, dict_) in decorated]
+
+ print " %-4s %-30s %-10s" % ("ID", "Name",
"Arch")
+ print "=%-4s=%-30s=%-10s" % ("="*4, "="*30,
"="*10)
+ if action: print action
+ for d in machines:
+ if isinstance(d, dict):
+ m = snake.machine.Machine(dict=d)
+ else:
+ m = d
+ print " %-4s %-30s %-50s" % (m.id, m.nickname, m.arch[0])
+
+# FIXME - perhaps move into snake/machine.py as `print t.info()` ?
+# FIXME - yes, some of this is ugly ... but it works
+def print_info(t):
+ '''Utility function to display formatted information about a
machine'''
+ for (k,v) in [["ID", t.id],
+ ["Nickname", t.nickname],
+ ["Arch", " ".join(t.arch)],
+ ["Fingerprints", " ".join(t.fingerprints)],
+ ["Kernel Args", t.bootargs],
+ ]:
+ print "%-12s : %s" % (k,v)
+ print ""
+
+def parse_machine_args(args):
+ '''examines the argument list and returns (nicklist,idlist), where
+ nicklist is all the given URIs and idlist is all of the given arguments
+ that are known machine IDs. As a special case, 'all' will expand to a list
+ of all known IDs. Other arguments are ignored.'''
+ nicklist = list()
+ idlist = list()
+ known_ids = [hasattr(t,'id') and t.id or t.get('id') for t in
snake_list(dict())]
+
+ # special case: for "all", return a list of all known machine ids.
+ if 'all' in args:
+ idlist = known_ids
+ # remove it from the arg list
+ while 'all' in args:
+ args.remove('all')
+
+ # Parse the rest of the args given.
+ for a in args:
+ if not a.isdigit():
+ nicklist.append(a)
+ elif a in known_ids and a not in idlist:
+ idlist.append(a)
+
+ return (nicklist,idlist)
+
+def setup_option_parser(cmd=None):
+ parser = None
+ if cmd in ["add", "register", "update"]:
+ if cmd == "update":
+ parser = optparse.OptionParser(usage="snake-machine [options] %s id
[%s-options] " % (cmd,cmd))
+ else:
+ parser = optparse.OptionParser(usage="snake-machine [options] %s
[%s-options]" % (cmd,cmd))
+
+ parser.add_option("-n", "--nickname",
+ action="store", dest="nickname", default=None,
+ help="Nickname of the machine (defaults to %default)")
+ parser.add_option("-a", "--arch",
+ action="append", dest="arch", default=None,
+ help="Default architecture (defaults to current OS)")
+ parser.add_option("-H", "--fingerprint",
+ action="append", dest="fingerprints",
default=None,
+ help="Unique fingerprints associated with this system (typically
mac addresses)")
+ parser.add_option("-b", "--bootargs",
+ action="store", dest="bootargs", default=None,
+ help="Kernel boot arguments required to install this
system")
+
+ elif cmd is None:
+ parser=optparse.OptionParser(usage="snake-machine [options] < %s
>" % (", ".join(client_commands)))
+ parser.add_option("-s", "--server",
+ action="store", dest="server",
default=os.environ.get("SNAKE_SERVER",""),
+ help="Specify the SNAKE server")
+ parser.add_option("-p", "--port",
+ action="store", dest="port",
default=os.environ.get("SNAKE_PORT","2903"),
+ help="The SNAKE xmlrpc port")
+ parser.add_option("-v", "--verbose",
+ action="store_true", dest="verbose",
default=False,
+ help="Output extra information")
+
+ # if we are accessing a local machinedb, offer a conffile cmdline parameter
+ if sys.modules.has_key("snake.machinedb"):
+ parser.add_option("-c", "--config",
+ action="store", dest="conffile",
default=snake.config.DEFAULT_CONFIGFILE,
+ help="Override default configuration file (only for local
system)")
+
+ else:
+ parser=optparse.OptionParser(usage="snake-machine [options] %s
[%s-options]" % (cmd,cmd))
+
+ return parser
+
+def main(argv):
+
+ global log, server, snake_list, snake_add, snake_update, snake_remove
+
+ parser = setup_option_parser()
+
+ # search for requested command
+ rIndex = len(argv)
+ for cmd in argv:
+ if cmd in client_commands:
+ rIndex = argv.index(cmd) + 1
+ break
+
+ (opt, args) = parser.parse_args(argv[1:rIndex])
+
+ # Set up logging
+ log = snake.log.setup_logger(opt.verbose and snake.log.DEBUG or snake.log.INFO)
+
+ # Use XMLRPC?
+ if opt.server:
+ if not snake.client.check_server(opt.server, opt.port):
+ '''the server is not responding'''
+ log.error("The snake server '%s:%s' is not responding. \
+ Please check your server URL and try again." % (opt.server, opt.port))
+ sys.exit(1)
+ server = snake.client.connect(opt.server, opt.port)
+ snake_list = server.machine.list
+ snake_add = server.machine.add
+ snake_update = server.machine.update
+ snake_remove = server.machine.remove
+ log.debug("Using machinedb on %s:%s as data source" % (opt.server,
opt.port))
+ else:
+ if not sys.modules.has_key("snake.machinedb"):
+ log.error("Unable to use local treedb, is snake-server
installed?")
+ sys.exit(1)
+
+ # load server configuration file
+ try:
+ snake.config.getconfig(filename=opt.conffile)
+ except Exception, e:
+ log.error("Error reading configuration from '%s'" %
opt.conffile)
+ log.debug(e)
+ sys.exit(1)
+
+ snake_list = snake.machinedb.dictmachines
+ snake_add = snake.machinedb.add
+ snake_update = snake.machinedb.update
+ snake_remove = snake.machinedb.remove
+ log.debug("Using local machinedb as data source")
+
+ # no command given
+ if len(args) == 0:
+ parser.error("You need to give some command")
+ sys.exit(1)
+
+ cmd = args[0]
+ cmdopts = argv[rIndex:]
+
+ # FIXME: try/except handling!
+ if cmd in ["add", "register"]:
+ cmd_parser = setup_option_parser(cmd)
+ (copt,cargs) = cmd_parser.parse_args(cmdopts)
+
+ log.debug("Adding '%s'" % (copt.nickname,))
+
+ # build out hwinfo dictionary
+ hwinfo = dict()
+ hwinfo["nickname"] = copt.nickname or gethostname()
+ hwinfo["arch"] = copt.arch or [rpmUtils.arch.getBaseArch()]
+ hwinfo["fingerprints"] = copt.fingerprints or
snake.machineinfo.get_fingerprints()
+ hwinfo["bootargs"] = copt.bootargs or ""
+
+ # time to add the machine
+ try:
+ snake_add(copt.nickname, hwinfo)
+ except Exception, e:
+ log.error("Failed to add machine %s - %s" % (copt.nickname,e))
+ if opt.verbose:
+ log.exception("Unhandled exception")
+ else:
+ print "%sed machine %s" % (cmd.capitalize(), copt.nickname,)
+
+ elif cmd == "update":
+ cmd_parser = setup_option_parser(cmd)
+ (copt,cargs) = cmd_parser.parse_args(cmdopts)
+
+ if len(cargs) == 1:
+ id = cargs.pop()
+ else:
+ parser.error("Required system id missing")
+ sys.exit(1)
+
+ # build out hwinfo dictionary
+ hwinfo = dict()
+ hwinfo["id"] = id
+ # only initialize a value if user provided on the cmdline (e.g. no default
values)
+ for key in ["nickname", "arch", "fingerprints",
"bootargs"]:
+ if getattr(copt, key) is not None:
+ hwinfo[key] = getattr(copt, key)
+
+ log.debug("Updating machine '%s' - %s" % (id,hwinfo))
+
+ # time to add the machine
+ try:
+ snake_update(id, hwinfo)
+ except Exception, e:
+ log.error("Failed to update machine id %s - %s" % (id,e))
+ if opt.verbose:
+ log.exception("Unhandled exception")
+ else:
+ print "Updated machine %s" % (id,)
+
+ elif cmd in ["list", "whoami"]:
+ cmd_parser = setup_option_parser(cmd)
+ (copt,cargs) = cmd_parser.parse_args(cmdopts)
+
+ filter = dict()
+ if cmd == "whoami":
+ filter["fingerprint"] = snake.machineinfo.get_fingerprints()
+ else:
+ for o in cargs:
+ if '=' in o:
+ k,v = o.split('=')
+ if filter.has_key(k):
+ if isinstance(filter[k], str):
+ filter[k] = [filter[k]]
+ filter[k].append(v)
+ else:
+ filter[k]=v
+
+ log.debug("Searching filter: %s" % filter)
+ machinelist = snake_list(filter)
+
+ if len(machinelist) == 0:
+ log.error("No matching machines to list")
+ sys.exit(1)
+
+ if cmd == "whoami":
+ if len(machinelist) > 1:
+ log.warn("Multiple matching registrations")
+ for local_m in machinelist:
+ m = snake.machine.Machine(dict=local_m)
+ print_info(m)
+ else:
+ print_table(machinelist)
+
+ elif cmd == "info":
+ (nicklist,idlist) = parse_machine_args(cmdopts)
+
+ if len(idlist) + len(nicklist) == 0:
+ log.error("No matching Machines to list")
+ sys.exit(1)
+
+ for id in idlist:
+ local_m = snake_list({'id':id}).pop()
+ m = snake.machine.Machine(dict=local_m)
+ print_info(m)
+
+ for u in nicklist:
+ try:
+ local_m = snake_list({'nickname':u}).pop()
+ m = snake.machine.Machine(dict=local_m)
+ print_info(m)
+ except Exception, e:
+ log.error("Not recognized as a valid nickname: %s" % u)
+
+ elif cmd == "remove":
+ cmd_parser = setup_option_parser(cmd)
+ (copt,cargs) = cmd_parser.parse_args(cmdopts)
+
+ if not cargs:
+ log.error("You need to supply a machine nickname or id.")
+ sys.exit(1)
+
+ # build a dict of {name: __dict__} for future removal
+ remove = dict()
+ for d in snake_list():
+ m = snake.machine.Machine(dict=d)
+ if m.nickname in cargs or m.id in cargs:
+ remove[m.id] = m
+
+ # report if a requested argument was not found
+ for o in cargs:
+ if not remove.has_key(o):
+ log.error("No match for argument: %s" % o)
+
+ # were any machines found for removal?
+ if not remove:
+ log.error("No machines marked for removal")
+ else:
+ print_table(remove.values(), "Removing:")
+ print ""
+
+ # prompt for user confirmation
+ if snake.client.userconfirm():
+ for (id, m) in remove.items():
+ try:
+ snake_remove(id)
+ except Exception,e:
+ log.error("Failed to remove %s: %s " % (m.nickname,e))
+ else:
+ print "Removed %s" % (m.nickname,)
+ else:
+ log.error("Exiting on user command")
+
+ elif cmd == "rename":
+ # process command-line arguments
+ cmd_parser = setup_option_parser(cmd)
+ (copt,cargs) = cmd_parser.parse_args(cmdopts)
+
+ if len(cargs) != 2:
+ cmd_parser.error("Incorrect arguments supplied")
+ sys.exit(1)
+
+ (oldname,newname) = cargs
+ filter = {"name": oldname}
+ kslist = snake_list(filter)
+
+ if len(kslist) == 0:
+ log.error("No matching kickstart list")
+ sys.exit(1)
+
+ # attempt rename
+ # FIXME - security? what if newname = /etc/passwd?
+ try:
+ snake_rename(oldname,newname)
+ except Exception,e:
+ log.error("Failed to rename %s: %s " % (oldname,e))
+ else:
+ print "Renamed %s to %s" % (oldname,newname)
+
+ elif cmd == "describe":
+ # process command-line arguments
+ cmd_parser = setup_option_parser(cmd)
+ (copt,cargs) = cmd_parser.parse_args(cmdopts)
+
+ if len(cargs) != 2:
+ cmd_parser.error("Incorrect arguments supplied")
+ sys.exit(1)
+
+ (name,desc) = cargs
+ filter = {"name": name}
+ kslist = snake_list(filter)
+
+ if len(kslist) == 0:
+ log.error("No matching kickstart found")
+ sys.exit(1)
+
+ # attempt update
+ try:
+ snake_describe(name,desc)
+ except Exception,e:
+ log.error("Failed to describe %s: %s " % (name,e))
+ else:
+ print "Updated description of %s: %s" % (name,desc)
+
+ else:
+ parser.error("Unrecognized command: %s" % cmd)
+ sys.exit(1)
+
+if __name__ == '__main__':
+ try:
+ main(sys.argv)
+ except KeyboardInterrupt:
+ pass
+
diff --git a/snake-server b/snake-server
index 9d3c331..65f9d82 100755
--- a/snake-server
+++ b/snake-server
@@ -1,7 +1,7 @@
#!/usr/bin/python
import os, sys, socket, optparse
-import snake.treedb, snake.ksdb
+import snake.treedb, snake.ksdb, snake.machinedb
import snake.log, snake.config
from DocXMLRPCServer import DocXMLRPCServer,DocXMLRPCRequestHandler
@@ -36,20 +36,32 @@ def kernel_args_list(argdata):
cmdline_args.append(ks.kernel_args)
# TODO : machine specific kernel_args?
+ if argdata.has_key("fingerprints") and argdata["fingerprints"]:
+ '''attempt to identify the client system based on
fingerprints'''
+ machines =
snake.machinedb.listmachines(fingerprint=argdata["fingerprints"])
+ if len(machines) == 1:
+ '''1 system found'''
+ cmdline_args.append(machines[0].bootargs)
+ #elif len(machines) > 1:
+ else:
+ log.warn("kernel_args_list found %s machines with matching fingerprints:
%s" % (len(machines), argdata["fingerprints"]))
# client-supplied specific kernel_args?
if argdata.has_key("cmdline") and argdata["cmdline"]:
cmdline_args.append(argdata["cmdline"])
- #else:
- # Kickstart specific kernel_args?
- # cmdline_args.append("method=%s" % argdata["uri"])
-
# if no networking provided, provide suitable defaults
- if cmdline_args.count("ksdevice=") == 0:
+ ksdevice_found = False
+ ip_found = False
+ for args in cmdline_args:
+ if not ksdevice_found:
+ ksdevice_found = args.count("ksdevice=") > 0
+ if not ip_found:
+ ip_found = args.count("ip=") > 0
+
+ if not ksdevice_found:
cmdline_args.append("ksdevice=link")
-
- if cmdline_args.count("ip=") == 0:
+ if not ip_found:
cmdline_args.append("ip=dhcp")
return " ".join(cmdline_args)
@@ -181,6 +193,10 @@ if __name__ == "__main__":
server.register_function(snake.treedb.removetree,'tree.remove')
server.register_function(snake.treedb.add,'uri.add')
server.register_function(snake.treedb.removeuri,'uri.remove')
+ server.register_function(snake.machinedb.dictmachines,'machine.list')
+ server.register_function(snake.machinedb.remove,'machine.remove')
+ server.register_function(snake.machinedb.add,'machine.add')
+ server.register_function(snake.machinedb.update,'machine.update')
# FIXME: add check function (like snake-tree)
server.register_function(snake.ksdb.listkickstarts,'kickstart.list')
server.register_function(snake.ksdb.write_data,'kickstart.add')
@@ -188,7 +204,7 @@ if __name__ == "__main__":
server.register_function(snake.ksdb.generate,'kickstart.generate')
server.register_function(snake.ksdb.rename,'kickstart.rename')
server.register_function(snake.ksdb.describe,'kickstart.describe')
- server.register_function(kernel_args_list,'kernel_args.list')
+ server.register_function(kernel_args_list,'kernel_args.generate')
# Let us introspect, friend.
server.register_function(version,'snake.version')
server.register_introspection_functions()
diff --git a/snake.spec b/snake.spec
index cad3e7f..6ca9ab1 100644
--- a/snake.spec
+++ b/snake.spec
@@ -82,6 +82,7 @@ fi
%{_sbindir}/snake-install
%{_sbindir}/snake-install-tui
%{_sbindir}/snake-tree
+%{_sbindir}/snake-machine
%{_sbindir}/snake-ks
%{_bindir}/snake-rawhide-status
%dir %{python_sitelib}/snake
@@ -92,6 +93,7 @@ fi
%{python_sitelib}/snake/install.py*
%{python_sitelib}/snake/log.py*
%{python_sitelib}/snake/machineinfo.py*
+%{python_sitelib}/snake/machine.py*
%{python_sitelib}/snake/saverestore.py*
%{python_sitelib}/snake/tui.py*
%{python_sitelib}/snake/tree.py*
@@ -125,7 +127,7 @@ fi
%{python_sitelib}/snake/kickstart.py*
%{python_sitelib}/snake/labindex.py*
%{python_sitelib}/snake/labquery.py*
-%{python_sitelib}/snake/machine.py*
+%{python_sitelib}/snake/machinedb.py*
%{python_sitelib}/snake/plugins.py*
%{python_sitelib}/snake/server.py*
%{python_sitelib}/snake/ksdb.py*
diff --git a/snake/config.py b/snake/config.py
index d3e608b..aaccd0e 100644
--- a/snake/config.py
+++ b/snake/config.py
@@ -48,7 +48,7 @@ defaults={
"templatedir": "/var/lib/snake/templates",
"ksdb_dir": "/var/lib/snake/kickstarts",
"treedb_dir": "/var/lib/snake/trees",
- "machinedbdir": "/var/lib/snake/machinedb",
+ "machinedb_dir": "/var/lib/snake/machines",
"logfile": "/var/log/snake",
"xmlcache_refresh": "10",
"puthandler_enabled": "no"
diff --git a/snake/machine.py b/snake/machine.py
index 951bedb..3eb536c 100644
--- a/snake/machine.py
+++ b/snake/machine.py
@@ -1,8 +1,6 @@
#
# machine.py - a Machine object for SNAKE
#
-# Will Woods <wwoods(a)redhat.com>
-#
# Copyright 2006, 2007 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use, modify,
@@ -20,67 +18,109 @@
# with the express permission of Red Hat, Inc.
#
-from snake.xmlhelper import indent, ET
+import ConfigParser
+from snake.util import uuidToString, randomUUID
+from socket import gethostname
class Machine(object):
def __init__(self,**kwargs):
- self.id=""
- self.nickname=""
- self.fqdn=""
- self.archlist=[]
- self.bootargs=""
- self.fingerprints=[]
- self.ksdevice=""
- self.hwinfo=[]
- if 'id' in kwargs:
- self.id = kwargs['id']
- if 'nickname' in kwargs:
- self.nickname = kwargs['nickname']
- if 'xml' in kwargs:
- self.parse(kwargs['xml'])
+ self.id = "" # A unique id to reference this system
+ self.nickname = "" # A human readable unique name
+ self.fqdn = "" # Fully qualified hostname
+ self.bootargs = "" # String of required boot arguments
+ self.arch = [] # List of supported architectures (first entry is
default)
+ self.fingerprints = [] # List of hwaddr's
+ self.hwinfo = []
+ self.config = ConfigParser.SafeConfigParser()
+ self.parse(**kwargs)
+
+ # Provide a convenience member alias for fingerprint <-> fingerprints
+ def __get_fingerprints__(self):
+ return self.fingerprints
+ def __set_fingerprints__(self, value):
+ self.fingerprints = value
+ fingerprint = property(__get_fingerprints__, None)
+
+ def parse(self,**kwargs):
+ if 'cfg' in kwargs:
+ self.parsemachineinfo(kwargs['cfg'])
+ elif 'xml' in kwargs:
+ self.parsexml(kwargs['xml'])
+ elif 'dict' in kwargs:
+ self.__dict__ = kwargs['dict']
+
+ def parsemachineinfo(self,cfg):
+ '''Parse a machineinfo file to fill in object attributes.
+ Argument may be either the path to a file, or an file object.'''
+
+ if hasattr(cfg,'readline'):
+ self.config.readfp(cfg) # read file-like object
+ else:
+ self.config.read(cfg) # try to read it as if it was a filename
- def parse(self,xml):
- elemtree = ET.parse(xml)
- r=elemtree.getroot()
- self.id = r.get('id') # machine id
- self.nickname = r.find('nickname').text # nickname
- self.fqdn = r.find('fqdn').text # fqdn
- self.bootargs=r.find('bootargs').text # bootargs
- for a in r.findall('arch'): # arch(es)
- self.archlist.append(a.text)
- for a in r.findall('fingerprint'): # fingerprints
- self.fingerprints.append(a.text)
- self.ksdevice = r.find('ksdevice').text # ksdevice
- return elemtree
+ # str fields
+ for opt in ["id", "nickname", "bootargs"]:
+ if self.config.has_option('general',opt):
+ setattr(self, opt, self.config.get("general", opt))
- def element(self,klist=()):
- def _traverse_element(self,child,elem):
- subelem = ET.SubElement(elem,child['elem'])
- if child.has_key('text') and child['text']:
- subelem.text = child['text']
- if child.has_key('attrib') and child['attrib']:
- subelem.attrib = child['attrib']
- for ch in child['children']: _traverse_element(ch,subelem)
- return
+ # list fields
+ for opt in ["arch", "fingerprints" ]:
+ if self.config.has_option('general',opt):
+ setattr(self, opt, self.config.get("general", opt).split())
- machine = ET.Element('machine',id=self.id) # id
- for type,set in {'nickname':self.nickname, 'fqdn':self.fqdn,\
- 'bootargs':self.bootargs}.items():
- ET.SubElement(machine,type).text = set
- for i in self.archlist: # arch(es)
- ET.SubElement(machine,'arch').text = i
- for i in self.fingerprints: # fingerprints
- ET.SubElement(machine,'fingerprint').text = i
- elem = ET.SubElement(machine,'ksdevice').text = self.ksdevice
- for i in self.hwinfo: # hwinfo
- elem = ET.SubElement(machine,i['elem'])
- if i.has_key('text') and i['text']:
- elem.text = i['text']
- if i.has_key('attrib') and i['attrib']:
- elem.attrib = i['attrib']
- for child in i['children']:
- _traverse_element(child,elem)
- return machine
-
- def xml(self):
- return ET.tostring(indent(self.element()))
+ self._fill_in_blanks()
+ return self.config
+
+ def _fill_in_blanks(self):
+ '''Fill in missing 'nickname' and 'id' attributes by
using uuidgen and
+ _makename methods'''
+ if not hasattr(self,'id') or not self.id:
+ self.id = uuidToString(randomUUID())
+ if not hasattr(self,'nickname') or not self.nickname:
+ self.nickname = gethostname() # note, this may not result in a unique
hostname
+
+# def parsexml(self,xml):
+# elemtree = ET.parse(xml)
+# r=elemtree.getroot()
+# self.id = r.get('id') # machine id
+# self.nickname = r.find('nickname').text # nickname
+# self.fqdn = r.find('fqdn').text # fqdn
+# self.bootargs=r.find('bootargs').text # bootargs
+# for a in r.findall('arch'): # arch(es)
+# self.archlist.append(a.text)
+# for a in r.findall('fingerprint'): # fingerprints
+# self.fingerprints.append(a.text)
+# self.ksdevice = r.find('ksdevice').text # ksdevice
+# return elemtree
+#
+# def element(self,klist=()):
+# def _traverse_element(self,child,elem):
+# subelem = ET.SubElement(elem,child['elem'])
+# if child.has_key('text') and child['text']:
+# subelem.text = child['text']
+# if child.has_key('attrib') and child['attrib']:
+# subelem.attrib = child['attrib']
+# for ch in child['children']: _traverse_element(ch,subelem)
+# return
+#
+# machine = ET.Element('machine',id=self.id) # id
+# for type,set in {'nickname':self.nickname, 'fqdn':self.fqdn,\
+# 'bootargs':self.bootargs}.items():
+# ET.SubElement(machine,type).text = set
+# for i in self.archlist: # arch(es)
+# ET.SubElement(machine,'arch').text = i
+# for i in self.fingerprints: # fingerprints
+# ET.SubElement(machine,'fingerprint').text = i
+# elem = ET.SubElement(machine,'ksdevice').text = self.ksdevice
+# for i in self.hwinfo: # hwinfo
+# elem = ET.SubElement(machine,i['elem'])
+# if i.has_key('text') and i['text']:
+# elem.text = i['text']
+# if i.has_key('attrib') and i['attrib']:
+# elem.attrib = i['attrib']
+# for child in i['children']:
+# _traverse_element(child,elem)
+# return machine
+#
+# def xml(self):
+# return ET.tostring(indent(self.element()))
diff --git a/snake/machinedb.py b/snake/machinedb.py
new file mode 100644
index 0000000..f9bf26c
--- /dev/null
+++ b/snake/machinedb.py
@@ -0,0 +1,209 @@
+#
+# snake/machinedb.py - functions for manipulating the machine database
+#
+# Copyright 2006, 2007 Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use, modify,
+# copy, or redistribute it subject to the terms and conditions of the GNU
+# General Public License v.2. This program is distributed in the hope that it
+# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
+# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU General Public License for more details.
+#
+# 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., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat
+# trademarks that are incorporated in the source code or documentation are not
+# subject to the GNU General Public License and may only be used or replicated
+# with the express permission of Red Hat, Inc.
+#
+
+import sys, os, time, tempfile
+import re
+import snake.machine
+from snake.config import getoption
+from ConfigParser import SafeConfigParser
+import logging
+
+def _loadmachines(dir=None,check=True):
+ '''Loads all the machineinfo files in the given dir. Returns a list of
+ Machine objects. '''
+ if dir is None:
+ dir = getoption("machinedb_dir")
+ machinelist = list()
+ for name in os.listdir(dir):
+ f = os.path.join(dir,name,"config")
+ if os.path.isfile(f):
+ t = snake.machine.Machine(cfg=f)
+ machinelist.append(t)
+ return machinelist
+
+def _writemachine(machine):
+ '''Write the given Machine object back to the machine
database'''
+ cfgdir = os.path.join(getoption("machinedb_dir"), str(machine.id))
+ if not os.path.isdir(cfgdir):
+ os.mkdir(cfgdir)
+
+ cfg = os.path.join(cfgdir, "config")
+ fp = open(cfg,"w")
+ machine.config.write(fp)
+ fp.close()
+
+def id_to_machine(id):
+ cfg = os.path.join(getoption("machinedb_dir"),"%s" % id,
"config")
+ if not os.path.exists(cfg):
+ return None
+ try:
+ m = snake.machine.Machine(cfg=cfg)
+ except ConfigParser.NoSectionError:
+ # File exists but there's nobody home. Bluh.
+ return None
+ return m
+
+def update(id, hwinfo):
+ '''Update an existing Machine with new information'''
+
+ # validate id
+ if not (isinstance(id, int) or (isinstance(id, str) and id.isdigit())):
+ raise ValueError("Unexpected machine id format: %s" % type(id))
+
+ cfgdir = os.path.join(getoption("machinedb_dir"), str(id))
+ cfg = os.path.join(cfgdir, "config")
+
+ # initialize a Machine() object
+ m = snake.machine.Machine()
+
+ # open up db record
+ if os.path.isfile(cfg):
+ logging.debug("Loading existing machineinfo '%s'" % cfg)
+ m.parse(cfg=cfg)
+
+ # if changing the nickname, is the new name already in use?
+ if hwinfo.has_key("nickname"):
+ matches = listmachines(nickname=hwinfo["nickname"])
+ if matches and True in map(lambda m: m.id != id, matches):
+ raise ValueError("A machine nickname '%s' already exists" %
hwinfo["nickname"])
+
+ # if changing fingerprints, are they already on file?
+ elif hwinfo.has_key("fingerprints") and
listmachines(fingerprint=hwinfo["fingerprints"]):
+ matches = listmachines(fingerprint=hwinfo["fingerprints"])
+ if matches and True in map(lambda m: m.id != id, matches):
+ raise ValueError("A machine with the supplied fingerprints %s already
exists" % hwinfo["fingerprints"])
+
+ # Store user-supplied values into Machine() object
+ for opt in ["id", "bootargs", "nickname",
"fingerprints", "arch"]:
+ if hwinfo.has_key(opt):
+ setattr(m, opt, hwinfo.get(opt))
+
+ # create a general section
+ if not m.config.has_section("general"):
+ m.config.add_section("general")
+
+ # update cfg object
+ for opt in ["id", "bootargs", "nickname",
"fingerprints", "arch"]:
+ if isinstance(getattr(m,opt), list):
+ m.config.set("general", opt, " ".join(getattr(m, opt)))
+ else:
+ m.config.set("general", opt, str(getattr(m, opt)))
+
+ logging.debug("Writing '%s' ...\n%s\n" % (cfg,
dict(m.config.items("general"))))
+ _writemachine(m)
+
+ return True
+
+def add(nickname, hwinfo):
+ '''Add the given Machine information to the database'''
+
+ id = 1
+ cfg = os.path.join(getoption("machinedb_dir"), str(id),
"config")
+ while (os.path.isfile(cfg)):
+ id = id + 1
+ cfg = os.path.join(getoption("machinedb_dir"), str(id),
"config")
+
+ hwinfo["id"] = id
+ hwinfo["nickname"] = nickname
+
+ return update(id, hwinfo)
+
+def get_ids(dir=None):
+ '''Return a list of known machine IDs.'''
+ if dir is None:
+ dir = getoption("machinedb_dir")
+ idlist = list()
+ for name in os.listdir(dir):
+ (id,ext) = os.path.splitext(name)
+ if ext != '.treeinfo':
+ continue
+ idlist.append(id)
+ return idlist
+
+def listmachines(**args):
+ '''Return a list of Machines that match all the given args. Recognized
args:
+ id: machine id
+ arch: architecture
+ nickname: machine name
+ ...and basically any other attribute of the Machine object.
+ '''
+ machinelist = _loadmachines()
+ resultlist = list()
+ for t in machinelist:
+ ok = True
+ for (k,v) in args.items():
+ if k in ["fingerprint", "arch"]:
+ '''match one or more fingerprints'''
+ # coerce as a list
+ if isinstance(v,str):
+ v = [v]
+ # for each fingerprint ... ensure we have a match
+ for f in v:
+ if f not in getattr(t, k):
+ ok = False
+ break
+ # attempt a regexp match
+ elif k.endswith("~") and re.search(v, getattr(t,k[:-1]), re.I) is
None:
+ ok = False
+ break
+ # attempt a regexp match
+ elif k.endswith("!") and re.search(v, getattr(t,k[:-1]), re.I) is
not None:
+ ok = False
+ break
+ elif (hasattr(t,k) and getattr(t,k) != v):
+ ok = False
+ break
+ if ok:
+ resultlist.append(t)
+ return resultlist
+
+def dictmachines(args={}):
+ '''get a list of Tree objects (in dict format) that match the given
args.
+ If no args are given, will return a list of all machines.
+ Common args:
+ arch (e.g. i386, ppc, x86_64...)
+ '''
+ machinelist = listmachines(**args)
+ for t in machinelist:
+ del t.config
+
+ return [t.__dict__ for t in machinelist]
+
+ # TODO? return a sorted dictionary
+ decorated = [(int(t.id), t.__dict__) for t in machinelist]
+ decorated.sort()
+ return [dict_ for (key, dict_) in decorated]
+
+def remove(id):
+ '''Remove the given machine from the machine database'''
+ # This is actually pretty easy.
+ cfgdir = os.path.join(getoption("machinedb_dir"), str(id))
+ cfg = os.path.join(cfgdir, "config")
+ if os.path.isfile(cfg):
+ os.unlink(cfg)
+
+ # remove the directory - FIXME (what about other files
+ if os.path.isdir(cfgdir):
+ os.rmdir(cfgdir)
+
+ logging.debug("Removed machine '%s'" % cfg)
+ return True # XMLRPC methods must return a value
+ else:
+ return False # XMLRPC methods must return a value
diff --git a/snake/machineinfo.py b/snake/machineinfo.py
index 1a8fec0..85350ec 100644
--- a/snake/machineinfo.py
+++ b/snake/machineinfo.py
@@ -42,8 +42,8 @@ def get_ksdevice(server):
def get_nics_from_ip():
'''Return dict {network interface:mac address} using ip
command'''
nics = {}
- # we don't care about vmnet? peth? vif? veth? xenbr? interfaces
- pattern1 = re.compile('^.+\\b(?:vmnet|peth|vif|veth|xenbr|virbr).+\\b.+$')
+ # we don't care about vmnet? peth? vif? veth? xenbr? pan? interfaces
+ pattern1 =
re.compile('^.+\\b(?:vmnet|peth|vif|veth|xenbr|virbr|pan).+\\b.+$')
pattern2 = re.compile('^.*\\blink/ether\\b.+$')
f = os.popen('LC_ALL=C /sbin/ip link list')
line1 = f.readline().strip()
diff --git a/snake/tui.py b/snake/tui.py
index 065d4aa..d613e3f 100644
--- a/snake/tui.py
+++ b/snake/tui.py
@@ -20,6 +20,7 @@ import os, sys
import optparse
import traceback
import rpmUtils.arch
+from socket import gethostname
import snack
from rhpl.translate import _, N_
@@ -29,6 +30,7 @@ from urlgrabber import progress
import snake.log
import snake.client
import snake.install
+import snake.machineinfo
from snake.constants import *
# import avahi
@@ -342,18 +344,21 @@ class BootLoaderAppendWindow(SnakeWindow):
def __call__(self):
+ supported_methods = self.tui.xmlrpc.system.listMethods()
+ fingerprints = snake.machineinfo.get_fingerprints()
+
# is the server suggesting any boot arguments?
- if "kernel_args.list" in self.tui.xmlrpc.system.listMethods():
+ if "kernel_args.generate" in supported_methods:
argdata = dict()
argdata['cmdline'] =
getattr(self.cfg,"cmdline","")
argdata['tree'] = self.cfg.tree.id
argdata['uri'] = self.cfg.uri
+ argdata['fingerprints'] = fingerprints
if self.cfg.ks:
argdata['ks'] = self.cfg.ks
- # TODO - add some sort of unique machine identifier so that the
- # server can provide machine specific cmdline args
- self.cfg.kernel_args = self.tui.xmlrpc.kernel_args.list(argdata)
+
+ self.cfg.kernel_args = self.tui.xmlrpc.kernel_args.generate(argdata)
else:
self.cfg.kernel_args = getattr(self.cfg, "cmdline", "")
@@ -372,14 +377,20 @@ class BootLoaderAppendWindow(SnakeWindow):
"kernel, enter them now. If you don't need any or "
"aren't sure, leave this blank."))
+ # if the server supports saving machine specific kernel args ...
+ cb = snack.Checkbox(_("Always use these options for this system?"))
+ if "machine.update" not in supported_methods:
+ cb.setFlags(snack.FLAG_DISABLED, snack.FLAGS_SET)
+
entry = snack.Entry(self.tui.width, scroll=1, returnExit=1,
text=self.cfg.kernel_args.strip())
buttons = snack.ButtonBar(self.screen, [TEXT_OK_BUTTON, TEXT_BACK_BUTTON,
TEXT_CANCEL_BUTTON])
# place widgets on a grid
- g = snack.GridFormHelp(self.screen, _("Boot Loader Configuration"),
None, 1, 3)
+ g = snack.GridFormHelp(self.screen, _("Boot Loader Configuration"),
None, 1, 4)
g.add(text, 0, 0, padding = (0, 0, 0, 1))
g.add(entry, 0, 1, padding = (0, 0, 0, 1))
- g.add(buttons, 0, 2, growx = 1)
+ g.add(cb, 0, 2, padding = (0, 0, 0, 1))
+ g.add(buttons, 0, 3, growx = 1)
# run the screen and gather the result
button = buttons.buttonPressed(g.runOnce(None, None))
@@ -389,6 +400,27 @@ class BootLoaderAppendWindow(SnakeWindow):
self.cfg.kernel_args = entry.value()
+ # asked to remember these values?
+ if cb.selected() \
+ and "machine.update" in supported_methods \
+ and "machine.add" in supported_methods \
+ and "machine.list" in supported_methods:
+
+ # Determine if a matching system registration exists?
+ machines = self.tui.xmlrpc.machine.list({"fingerprints":
fingerprints})
+ if len(machines) == 1:
+ m = machines.pop()
+ self.tui.xmlrpc.machine.update(m.get("id"),
{"bootargs": self.cfg.kernel_args})
+
+ # otherwise, quietly register the current system
+ elif len(machines) == 0:
+ self.tui.xmlrpc.machine.add(gethostname(), \
+ {"bootargs": self.cfg.kernel_args, \
+ "fingerprints": fingerprints, \
+ "arch": rpmUtils.arch.getBaseArch()})
+ else:
+ raise Exception("Unable to save boot arguments - multiple matches
for supplied fingerprints %s" % fingerprints)
+
return button
class ReviewWindow(SnakeWindow):
diff --git a/snake/util.py b/snake/util.py
index 4fc094b..f0f9d88 100644
--- a/snake/util.py
+++ b/snake/util.py
@@ -20,6 +20,7 @@
import os
import sys
+import random
# We cache the contents of /etc/mtab ... the following variables are used
# to keep our cache in sync
@@ -116,3 +117,21 @@ except ImportError, e:
cmdobj = popen2.Popen4(*popenargs)
# (pout, pin, perr) = (cmdobj.fromchild, cmdobj.tochild, cmdobj.childerr)
return cmdobj.wait()
+
+# the following three functions are from xend/uuid.py and are thus
+# available under the LGPL,
+# Copyright 2005 Mike Wray <mike.wray(a)hp.com>
+# Copyright 2005 XenSource Ltd
+def randomUUID():
+ """Generate a random UUID."""
+ return [ random.randint(0, 255) for _ in range(0, 16) ]
+
+def uuidToString(u):
+ return "-".join(["%02x" * 4, "%02x" * 2,
"%02x" * 2, "%02x" * 2,
+ "%02x" * 6]) % tuple(u)
+
+def uuidFromString(s):
+ s = s.replace('-', '')
+ return [ int(s[i : i + 2], 16) for i in range(0, 32, 2) ]
+
+
--
1.5.4.5