1 # Copyright (c) 2008,2009 Citrix Systems, Inc.
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU Lesser General Public License as published
5 # by the Free Software Foundation; version 2.1 only. with the special
6 # exception on linking described in file LICENSE.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU Lesser General Public License for more details.
16 from xml.dom.minidom import getDOMImplementation
17 from xml.dom.minidom import parse as parseXML
30 class Error(Exception):
31 def __init__(self, msg):
32 Exception.__init__(self)
36 # Run external utilities
39 def run_command(command):
40 log("Running command: " + ' '.join(command))
41 rc = os.spawnl(os.P_WAIT, command[0], *command)
43 log("Command failed %d: " % rc + ' '.join(command))
48 # Configuration File Handling.
51 class ConfigurationFile(object):
52 """Write a file, tracking old and new versions.
54 Supports writing a new version of a file and applying and
55 reverting those changes.
58 __STATE = {"OPEN":"OPEN",
59 "NOT-APPLIED":"NOT-APPLIED", "APPLIED":"APPLIED",
60 "REVERTED":"REVERTED", "COMMITTED": "COMMITTED"}
62 def __init__(self, path):
63 dirname,basename = os.path.split(path)
65 self.__state = self.__STATE['OPEN']
68 self.__path = os.path.join(dirname, basename)
69 self.__oldpath = os.path.join(dirname, "." + basename + ".xapi-old")
70 self.__newpath = os.path.join(dirname, "." + basename + ".xapi-new")
72 self.__f = open(self.__newpath, "w")
74 def attach_child(self, child):
75 self.__children.append(child)
82 return open(self.path()).readlines()
86 def write(self, args):
87 if self.__state != self.__STATE['OPEN']:
88 raise Error("Attempt to write to file in state %s" % self.__state)
92 if self.__state != self.__STATE['OPEN']:
93 raise Error("Attempt to close file in state %s" % self.__state)
96 self.__state = self.__STATE['NOT-APPLIED']
99 if self.__state != self.__STATE['NOT-APPLIED']:
100 raise Error("Attempt to compare file in state %s" % self.__state)
105 if self.__state != self.__STATE['NOT-APPLIED']:
106 raise Error("Attempt to apply configuration from state %s" % self.__state)
108 for child in self.__children:
111 log("Applying changes to %s configuration" % self.__path)
113 # Remove previous backup.
114 if os.access(self.__oldpath, os.F_OK):
115 os.unlink(self.__oldpath)
117 # Save current configuration.
118 if os.access(self.__path, os.F_OK):
119 os.link(self.__path, self.__oldpath)
120 os.unlink(self.__path)
122 # Apply new configuration.
123 assert(os.path.exists(self.__newpath))
124 os.link(self.__newpath, self.__path)
126 # Remove temporary file.
127 os.unlink(self.__newpath)
129 self.__state = self.__STATE['APPLIED']
132 if self.__state != self.__STATE['APPLIED']:
133 raise Error("Attempt to revert configuration from state %s" % self.__state)
135 for child in self.__children:
138 log("Reverting changes to %s configuration" % self.__path)
140 # Remove existing new configuration
141 if os.access(self.__newpath, os.F_OK):
142 os.unlink(self.__newpath)
144 # Revert new configuration.
145 if os.access(self.__path, os.F_OK):
146 os.link(self.__path, self.__newpath)
147 os.unlink(self.__path)
149 # Revert to old configuration.
150 if os.access(self.__oldpath, os.F_OK):
151 os.link(self.__oldpath, self.__path)
152 os.unlink(self.__oldpath)
154 # Leave .*.xapi-new as an aid to debugging.
156 self.__state = self.__STATE['REVERTED']
159 if self.__state != self.__STATE['APPLIED']:
160 raise Error("Attempt to commit configuration from state %s" % self.__state)
162 for child in self.__children:
165 log("Committing changes to %s configuration" % self.__path)
167 if os.access(self.__oldpath, os.F_OK):
168 os.unlink(self.__oldpath)
169 if os.access(self.__newpath, os.F_OK):
170 os.unlink(self.__newpath)
172 self.__state = self.__STATE['COMMITTED']
175 # Helper functions for encoding/decoding database attributes to/from XML.
178 def _str_to_xml(xml, parent, tag, val):
179 e = xml.createElement(tag)
180 parent.appendChild(e)
181 v = xml.createTextNode(val)
183 def _str_from_xml(n):
184 def getText(nodelist):
186 for node in nodelist:
187 if node.nodeType == node.TEXT_NODE:
190 return getText(n.childNodes).strip()
192 def _bool_to_xml(xml, parent, tag, val):
194 _str_to_xml(xml, parent, tag, "True")
196 _str_to_xml(xml, parent, tag, "False")
197 def _bool_from_xml(n):
204 raise Error("Unknown boolean value %s" % s)
206 def _strlist_to_xml(xml, parent, ltag, itag, val):
207 e = xml.createElement(ltag)
208 parent.appendChild(e)
210 c = xml.createElement(itag)
212 cv = xml.createTextNode(v)
214 def _strlist_from_xml(n, ltag, itag):
216 for n in n.childNodes:
217 if n.nodeName == itag:
218 ret.append(_str_from_xml(n))
221 def _otherconfig_to_xml(xml, parent, val, attrs):
222 otherconfig = xml.createElement("other_config")
223 parent.appendChild(otherconfig)
224 for n,v in val.items():
226 raise Error("Unknown other-config attribute: %s" % n)
227 _str_to_xml(xml, otherconfig, n, v)
228 def _otherconfig_from_xml(n, attrs):
230 for n in n.childNodes:
231 if n.nodeName in attrs:
232 ret[n.nodeName] = _str_from_xml(n)
236 # Definitions of the database objects (and their attributes) used by interface-reconfigure.
238 # Each object is defined by a dictionary mapping an attribute name in
239 # the xapi database to a tuple containing two items:
240 # - a function which takes this attribute and encodes it as XML.
241 # - a function which takes XML and decocdes it into a value.
243 # other-config attributes are specified as a simple array of strings
246 _VLAN_XML_TAG = "vlan"
247 _BOND_XML_TAG = "bond"
248 _NETWORK_XML_TAG = "network"
250 _ETHTOOL_OTHERCONFIG_ATTRS = ['ethtool-%s' % x for x in 'autoneg', 'speed', 'duplex', 'rx', 'tx', 'sg', 'tso', 'ufo', 'gso' ]
252 _PIF_OTHERCONFIG_ATTRS = [ 'domain', 'peerdns', 'defaultroute', 'mtu', 'static-routes' ] + \
253 [ 'bond-%s' % x for x in 'mode', 'miimon', 'downdelay', 'updelay', 'use_carrier' ] + \
254 _ETHTOOL_OTHERCONFIG_ATTRS
256 _PIF_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
257 'management': (_bool_to_xml,_bool_from_xml),
258 'network': (_str_to_xml,_str_from_xml),
259 'device': (_str_to_xml,_str_from_xml),
260 'bond_master_of': (lambda x, p, t, v: _strlist_to_xml(x, p, 'bond_master_of', 'slave', v),
261 lambda n: _strlist_from_xml(n, 'bond_master_of', 'slave')),
262 'bond_slave_of': (_str_to_xml,_str_from_xml),
263 'VLAN': (_str_to_xml,_str_from_xml),
264 'VLAN_master_of': (_str_to_xml,_str_from_xml),
265 'VLAN_slave_of': (lambda x, p, t, v: _strlist_to_xml(x, p, 'VLAN_slave_of', 'master', v),
266 lambda n: _strlist_from_xml(n, 'VLAN_slave_Of', 'master')),
267 'ip_configuration_mode': (_str_to_xml,_str_from_xml),
268 'IP': (_str_to_xml,_str_from_xml),
269 'netmask': (_str_to_xml,_str_from_xml),
270 'gateway': (_str_to_xml,_str_from_xml),
271 'DNS': (_str_to_xml,_str_from_xml),
272 'MAC': (_str_to_xml,_str_from_xml),
273 'other_config': (lambda x, p, t, v: _otherconfig_to_xml(x, p, v, _PIF_OTHERCONFIG_ATTRS),
274 lambda n: _otherconfig_from_xml(n, _PIF_OTHERCONFIG_ATTRS)),
276 # Special case: We write the current value
277 # PIF.currently-attached to the cache but since it will
278 # not be valid when we come to use the cache later
279 # (i.e. after a reboot) we always read it as False.
280 'currently_attached': (_bool_to_xml, lambda n: False),
283 _VLAN_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
284 'tagged_PIF': (_str_to_xml,_str_from_xml),
285 'untagged_PIF': (_str_to_xml,_str_from_xml),
288 _BOND_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
289 'master': (_str_to_xml,_str_from_xml),
290 'slaves': (lambda x, p, t, v: _strlist_to_xml(x, p, 'slaves', 'slave', v),
291 lambda n: _strlist_from_xml(n, 'slaves', 'slave')),
294 _NETWORK_OTHERCONFIG_ATTRS = [ 'mtu', 'static-routes' ] + _ETHTOOL_OTHERCONFIG_ATTRS
296 _NETWORK_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
297 'bridge': (_str_to_xml,_str_from_xml),
298 'PIFs': (lambda x, p, t, v: _strlist_to_xml(x, p, 'PIFs', 'PIF', v),
299 lambda n: _strlist_from_xml(n, 'PIFs', 'PIF')),
300 'other_config': (lambda x, p, t, v: _otherconfig_to_xml(x, p, v, _NETWORK_OTHERCONFIG_ATTRS),
301 lambda n: _otherconfig_from_xml(n, _NETWORK_OTHERCONFIG_ATTRS)),
305 # Database Cache object
311 assert(_db is not None)
314 def db_init_from_cache(cache):
317 _db = DatabaseCache(cache_file=cache)
319 def db_init_from_xenapi(session):
322 _db = DatabaseCache(session_ref=session)
324 class DatabaseCache(object):
325 def __read_xensource_inventory(self):
326 filename = "/etc/xensource-inventory"
327 f = open(filename, "r")
328 lines = [x.strip("\n") for x in f.readlines()]
331 defs = [ (l[:l.find("=")], l[(l.find("=") + 1):]) for l in lines ]
332 defs = [ (a, b.strip("'")) for (a,b) in defs ]
335 def __pif_on_host(self,pif):
336 return self.__pifs.has_key(pif)
338 def __get_pif_records_from_xapi(self, session, host):
340 for (p,rec) in session.xenapi.PIF.get_all_records().items():
341 if rec['host'] != host:
345 self.__pifs[p][f] = rec[f]
346 self.__pifs[p]['other_config'] = {}
347 for f in _PIF_OTHERCONFIG_ATTRS:
348 if not rec['other_config'].has_key(f): continue
349 self.__pifs[p]['other_config'][f] = rec['other_config'][f]
351 def __get_vlan_records_from_xapi(self, session):
353 for v in session.xenapi.VLAN.get_all():
354 rec = session.xenapi.VLAN.get_record(v)
355 if not self.__pif_on_host(rec['untagged_PIF']):
358 for f in _VLAN_ATTRS:
359 self.__vlans[v][f] = rec[f]
361 def __get_bond_records_from_xapi(self, session):
363 for b in session.xenapi.Bond.get_all():
364 rec = session.xenapi.Bond.get_record(b)
365 if not self.__pif_on_host(rec['master']):
368 for f in _BOND_ATTRS:
369 self.__bonds[b][f] = rec[f]
371 def __get_network_records_from_xapi(self, session):
373 for n in session.xenapi.network.get_all():
374 rec = session.xenapi.network.get_record(n)
375 self.__networks[n] = {}
376 for f in _NETWORK_ATTRS:
378 # drop PIFs on other hosts
379 self.__networks[n][f] = [p for p in rec[f] if self.__pif_on_host(p)]
381 self.__networks[n][f] = rec[f]
382 self.__networks[n]['other_config'] = {}
383 for f in _NETWORK_OTHERCONFIG_ATTRS:
384 if not rec['other_config'].has_key(f): continue
385 self.__networks[n]['other_config'][f] = rec['other_config'][f]
387 def __to_xml(self, xml, parent, key, ref, rec, attrs):
388 """Encode a database object as XML"""
389 e = xml.createElement(key)
390 parent.appendChild(e)
392 e.setAttribute('ref', ref)
394 for n,v in rec.items():
399 raise Error("Unknown attribute %s" % n)
400 def __from_xml(self, e, attrs):
401 """Decode a database object from XML"""
402 ref = e.attributes['ref'].value
404 for n in e.childNodes:
405 if n.nodeName in attrs:
406 _,h = attrs[n.nodeName]
407 rec[n.nodeName] = h(n)
410 def __init__(self, session_ref=None, cache_file=None):
411 if session_ref and cache_file:
412 raise Error("can't specify session reference and cache file")
413 if cache_file == None:
415 session = XenAPI.xapi_local()
418 log("No session ref given on command line, logging in.")
419 session.xenapi.login_with_password("root", "")
421 session._session = session_ref
425 inventory = self.__read_xensource_inventory()
426 assert(inventory.has_key('INSTALLATION_UUID'))
427 log("host uuid is %s" % inventory['INSTALLATION_UUID'])
429 host = session.xenapi.host.get_by_uuid(inventory['INSTALLATION_UUID'])
431 self.__get_pif_records_from_xapi(session, host)
433 self.__get_vlan_records_from_xapi(session)
434 self.__get_bond_records_from_xapi(session)
435 self.__get_network_records_from_xapi(session)
438 session.xenapi.session.logout()
440 log("Loading xapi database cache from %s" % cache_file)
442 xml = parseXML(cache_file)
449 assert(len(xml.childNodes) == 1)
450 toplevel = xml.childNodes[0]
452 assert(toplevel.nodeName == "xenserver-network-configuration")
454 for n in toplevel.childNodes:
455 if n.nodeName == "#text":
457 elif n.nodeName == _PIF_XML_TAG:
458 (ref,rec) = self.__from_xml(n, _PIF_ATTRS)
459 self.__pifs[ref] = rec
460 elif n.nodeName == _BOND_XML_TAG:
461 (ref,rec) = self.__from_xml(n, _BOND_ATTRS)
462 self.__bonds[ref] = rec
463 elif n.nodeName == _VLAN_XML_TAG:
464 (ref,rec) = self.__from_xml(n, _VLAN_ATTRS)
465 self.__vlans[ref] = rec
466 elif n.nodeName == _NETWORK_XML_TAG:
467 (ref,rec) = self.__from_xml(n, _NETWORK_ATTRS)
468 self.__networks[ref] = rec
470 raise Error("Unknown XML element %s" % n.nodeName)
472 def save(self, cache_file):
474 xml = getDOMImplementation().createDocument(
475 None, "xenserver-network-configuration", None)
476 for (ref,rec) in self.__pifs.items():
477 self.__to_xml(xml, xml.documentElement, _PIF_XML_TAG, ref, rec, _PIF_ATTRS)
478 for (ref,rec) in self.__bonds.items():
479 self.__to_xml(xml, xml.documentElement, _BOND_XML_TAG, ref, rec, _BOND_ATTRS)
480 for (ref,rec) in self.__vlans.items():
481 self.__to_xml(xml, xml.documentElement, _VLAN_XML_TAG, ref, rec, _VLAN_ATTRS)
482 for (ref,rec) in self.__networks.items():
483 self.__to_xml(xml, xml.documentElement, _NETWORK_XML_TAG, ref, rec,
486 f = open(cache_file, 'w')
487 f.write(xml.toprettyxml())
490 def get_pif_by_uuid(self, uuid):
491 pifs = map(lambda (ref,rec): ref,
492 filter(lambda (ref,rec): uuid == rec['uuid'],
493 self.__pifs.items()))
495 raise Error("Unknown PIF \"%s\"" % uuid)
497 raise Error("Non-unique PIF \"%s\"" % uuid)
501 def get_pifs_by_device(self, device):
502 return map(lambda (ref,rec): ref,
503 filter(lambda (ref,rec): rec['device'] == device,
504 self.__pifs.items()))
506 def get_pif_by_bridge(self, bridge):
507 networks = map(lambda (ref,rec): ref,
508 filter(lambda (ref,rec): rec['bridge'] == bridge,
509 self.__networks.items()))
510 if len(networks) == 0:
511 raise Error("No matching network \"%s\"" % bridge)
514 for network in networks:
515 nwrec = self.get_network_record(network)
516 for pif in nwrec['PIFs']:
517 pifrec = self.get_pif_record(pif)
519 raise Error("Multiple PIFs on host for network %s" % (bridge))
522 raise Error("No PIF on host for network %s" % (bridge))
525 def get_pif_record(self, pif):
526 if self.__pifs.has_key(pif):
527 return self.__pifs[pif]
528 raise Error("Unknown PIF \"%s\"" % pif)
529 def get_all_pifs(self):
531 def pif_exists(self, pif):
532 return self.__pifs.has_key(pif)
534 def get_management_pif(self):
535 """ Returns the management pif on host
537 all = self.get_all_pifs()
539 pifrec = self.get_pif_record(pif)
540 if pifrec['management']: return pif
543 def get_network_record(self, network):
544 if self.__networks.has_key(network):
545 return self.__networks[network]
546 raise Error("Unknown network \"%s\"" % network)
548 def get_bond_record(self, bond):
549 if self.__bonds.has_key(bond):
550 return self.__bonds[bond]
554 def get_vlan_record(self, vlan):
555 if self.__vlans.has_key(vlan):
556 return self.__vlans[vlan]
564 def ethtool_settings(oc):
566 if oc.has_key('ethtool-speed'):
567 val = oc['ethtool-speed']
568 if val in ["10", "100", "1000"]:
569 settings += ['speed', val]
571 log("Invalid value for ethtool-speed = %s. Must be 10|100|1000." % val)
572 if oc.has_key('ethtool-duplex'):
573 val = oc['ethtool-duplex']
574 if val in ["10", "100", "1000"]:
575 settings += ['duplex', 'val']
577 log("Invalid value for ethtool-duplex = %s. Must be half|full." % val)
578 if oc.has_key('ethtool-autoneg'):
579 val = oc['ethtool-autoneg']
580 if val in ["true", "on"]:
581 settings += ['autoneg', 'on']
582 elif val in ["false", "off"]:
583 settings += ['autoneg', 'off']
585 log("Invalid value for ethtool-autoneg = %s. Must be on|true|off|false." % val)
587 for opt in ("rx", "tx", "sg", "tso", "ufo", "gso"):
588 if oc.has_key("ethtool-" + opt):
589 val = oc["ethtool-" + opt]
590 if val in ["true", "on"]:
591 offload += [opt, 'on']
592 elif val in ["false", "off"]:
593 offload += [opt, 'off']
595 log("Invalid value for ethtool-%s = %s. Must be on|true|off|false." % (opt, val))
596 return settings,offload
599 if oc.has_key('mtu'):
601 int(oc['mtu']) # Check that the value is an integer
603 except ValueError, x:
604 log("Invalid value for mtu = %s" % oc['mtu'])
608 # IP Network Devices -- network devices with IP configuration
610 def pif_ipdev_name(pif):
611 """Return the ipdev name associated with pif"""
612 pifrec = db().get_pif_record(pif)
613 nwrec = db().get_network_record(pifrec['network'])
616 # TODO: sanity check that nwrec['bridgeless'] != 'true'
617 return nwrec['bridge']
619 # TODO: sanity check that nwrec['bridgeless'] == 'true'
620 return pif_netdev_name(pif)
623 # Bare Network Devices -- network devices without IP configuration
626 def netdev_exists(netdev):
627 return os.path.exists("/sys/class/net/" + netdev)
629 def pif_netdev_name(pif):
630 """Get the netdev name for a PIF."""
632 pifrec = db().get_pif_record(pif)
635 return "%(device)s.%(VLAN)s" % pifrec
637 return pifrec['device']
642 def pif_is_bond(pif):
643 pifrec = db().get_pif_record(pif)
645 return len(pifrec['bond_master_of']) > 0
647 def pif_get_bond_masters(pif):
648 """Returns a list of PIFs which are bond masters of this PIF"""
650 pifrec = db().get_pif_record(pif)
652 bso = pifrec['bond_slave_of']
654 # bond-slave-of is currently a single reference but in principle a
655 # PIF could be a member of several bonds which are not
656 # concurrently attached. Be robust to this possibility.
657 if not bso or bso == "OpaqueRef:NULL":
659 elif not type(bso) == list:
662 bondrecs = [db().get_bond_record(bond) for bond in bso]
663 bondrecs = [rec for rec in bondrecs if rec]
665 return [bond['master'] for bond in bondrecs]
667 def pif_get_bond_slaves(pif):
668 """Returns a list of PIFs which make up the given bonded pif."""
670 pifrec = db().get_pif_record(pif)
672 bmo = pifrec['bond_master_of']
674 raise Error("Bond-master-of contains too many elements")
679 bondrec = db().get_bond_record(bmo[0])
681 raise Error("No bond record for bond master PIF")
683 return bondrec['slaves']
689 def pif_is_vlan(pif):
690 return db().get_pif_record(pif)['VLAN'] != '-1'
692 def pif_get_vlan_slave(pif):
693 """Find the PIF which is the VLAN slave of pif.
695 Returns the 'physical' PIF underneath the a VLAN PIF @pif."""
697 pifrec = db().get_pif_record(pif)
699 vlan = pifrec['VLAN_master_of']
700 if not vlan or vlan == "OpaqueRef:NULL":
701 raise Error("PIF is not a VLAN master")
703 vlanrec = db().get_vlan_record(vlan)
705 raise Error("No VLAN record found for PIF")
707 return vlanrec['tagged_PIF']
709 def pif_get_vlan_masters(pif):
710 """Returns a list of PIFs which are VLANs on top of the given pif."""
712 pifrec = db().get_pif_record(pif)
713 vlans = [db().get_vlan_record(v) for v in pifrec['VLAN_slave_of']]
714 return [v['untagged_PIF'] for v in vlans if v and db().pif_exists(v['untagged_PIF'])]
717 # Datapath base class
720 class Datapath(object):
721 """Object encapsulating the actions necessary to (de)configure the
722 datapath for a given PIF. Does not include configuration of the
723 IP address on the ipdev.
726 def __init__(self, pif):
729 def configure_ipdev(self, cfg):
730 """Write ifcfg TYPE field for an IPdev, plus any type specific
733 raise NotImplementedError
735 def preconfigure(self, parent):
736 """Prepare datapath configuration for PIF, but do not actually
739 Any configuration files should be attached to parent.
741 raise NotImplementedError
743 def bring_down_existing(self):
744 """Tear down any existing network device configuration which
745 needs to be undone in order to bring this PIF up.
747 raise NotImplementedError
750 """Apply the configuration prepared in the preconfigure stage.
752 Should assume any configuration files changed attached in
753 the preconfigure stage are applied and bring up the
754 necesary devices to provide the datapath for the
757 Should not bring up the IPdev.
759 raise NotImplementedError
762 """Called after the IPdev has been brought up.
764 Should do any final setup, including reinstating any
765 devices which were taken down in the bring_down_existing
768 raise NotImplementedError
770 def bring_down(self):
771 """Tear down and deconfigure the datapath. Should assume the
772 IPdev has already been brought down.
774 raise NotImplementedError
776 def DatapathFactory(pif):
777 # XXX Need a datapath object for bridgeless PIFs
780 network_conf = open("/etc/xensource/network.conf", 'r')
781 network_backend = network_conf.readline().strip()
784 raise Error("failed to determine network backend:" + e)
786 if network_backend == "bridge":
787 from InterfaceReconfigureBridge import DatapathBridge
788 return DatapathBridge(pif)
789 elif network_backend == "vswitch":
790 from InterfaceReconfigureVswitch import DatapathVswitch
791 return DatapathVswitch(pif)
793 raise Error("unknown network backend %s" % network_backend)