xenserver: Synchronize interface-reconfigure from Citrix.
[sliver-openvswitch.git] / xenserver / opt_xensource_libexec_interface-reconfigure
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2008,2009 Citrix Systems, Inc. All rights reserved.
4 # Copyright (c) 2009 Nicira Networks.
5 #
6 """Usage:
7
8     %(command-name)s <PIF> up
9     %(command-name)s <PIF> down
10     %(command-name)s [<PIF>] rewrite
11     %(command-name)s --force <BRIDGE> up
12     %(command-name)s --force <BRIDGE> down
13     %(command-name)s --force <BRIDGE> rewrite --device=<INTERFACE> <CONFIG>
14     %(command-name)s --force all down
15
16     where <PIF> is one of:
17        --session <SESSION-REF> --pif <PIF-REF>
18        --pif-uuid <PIF-UUID>
19     and <CONFIG> is one of:
20        --mode=dhcp
21        --mode=static --ip=<IPADDR> --netmask=<NM> [--gateway=<GW>]
22
23   Options:
24     --session           A session reference to use to access the xapi DB
25     --pif               A PIF reference within the session.
26     --pif-uuid          The UUID of a PIF.
27     --force             An interface name.
28 """
29
30 #
31 # Undocumented parameters for test & dev:
32 #
33 #  --output-directory=<DIR>     Write configuration to <DIR>. Also disables actually
34 #                               raising/lowering the interfaces
35 #
36 #
37 #
38 # Notes:
39 # 1. Every pif belongs to exactly one network
40 # 2. Every network has zero or one pifs
41 # 3. A network may have an associated bridge, allowing vifs to be attached
42 # 4. A network may be bridgeless (there's no point having a bridge over a storage pif)
43
44 import XenAPI
45 import os, sys, getopt, time, signal
46 import syslog
47 import traceback
48 import time
49 import re
50 import random
51 from xml.dom.minidom import getDOMImplementation
52 from xml.dom.minidom import parse as parseXML
53
54 output_directory = None
55
56 db = None
57 management_pif = None
58
59 vswitch_state_dir = "/var/lib/openvswitch/"
60 dbcache_file = vswitch_state_dir + "dbcache"
61
62 class Usage(Exception):
63     def __init__(self, msg):
64         Exception.__init__(self)
65         self.msg = msg
66
67 class Error(Exception):
68     def __init__(self, msg):
69         Exception.__init__(self)
70         self.msg = msg
71
72 class ConfigurationFile(object):
73     """Write a file, tracking old and new versions.
74
75     Supports writing a new version of a file and applying and
76     reverting those changes.
77     """
78
79     __STATE = {"OPEN":"OPEN",
80                "NOT-APPLIED":"NOT-APPLIED", "APPLIED":"APPLIED",
81                "REVERTED":"REVERTED", "COMMITTED": "COMMITTED"}
82     
83     def __init__(self, fname, path="/etc/sysconfig/network-scripts"):
84
85         self.__state = self.__STATE['OPEN']
86         self.__fname = fname
87         self.__children = []
88         
89         if debug_mode():
90             dirname = output_directory
91         else:
92             dirname = path
93             
94         self.__path    = os.path.join(dirname, fname)
95         self.__oldpath = os.path.join(dirname, "." + fname + ".xapi-old")
96         self.__newpath = os.path.join(dirname, "." + fname + ".xapi-new")
97         self.__unlink = False
98         
99         self.__f = open(self.__newpath, "w")
100
101     def attach_child(self, child):
102         self.__children.append(child)
103
104     def path(self):
105         return self.__path
106
107     def readlines(self):
108         try:
109             return open(self.path()).readlines()
110         except:
111             return ""
112         
113     def write(self, args):
114         if self.__state != self.__STATE['OPEN']:
115             raise Error("Attempt to write to file in state %s" % self.__state)
116         self.__f.write(args)
117
118     def unlink(self):
119         if self.__state != self.__STATE['OPEN']:
120             raise Error("Attempt to unlink file in state %s" % self.__state)
121         self.__unlink = True
122         self.__f.close()
123         self.__state = self.__STATE['NOT-APPLIED']
124     
125     def close(self):
126         if self.__state != self.__STATE['OPEN']:
127             raise Error("Attempt to close file in state %s" % self.__state)
128         
129         self.__f.close()
130         self.__state = self.__STATE['NOT-APPLIED']
131
132     def changed(self):
133         if self.__state != self.__STATE['NOT-APPLIED']:
134             raise Error("Attempt to compare file in state %s" % self.__state)
135
136         return True
137
138     def apply(self):
139         if self.__state != self.__STATE['NOT-APPLIED']:
140             raise Error("Attempt to apply configuration from state %s" % self.__state)
141
142         for child in self.__children:
143             child.apply()
144
145         log("Applying changes to %s configuration" % self.__fname)
146
147         # Remove previous backup.
148         if os.access(self.__oldpath, os.F_OK):
149             os.unlink(self.__oldpath)
150
151         # Save current configuration.
152         if os.access(self.__path, os.F_OK):
153             os.link(self.__path, self.__oldpath)
154             os.unlink(self.__path)
155
156         # Apply new configuration.
157         assert(os.path.exists(self.__newpath))
158         if not self.__unlink:
159             os.link(self.__newpath, self.__path)
160         else:
161             pass # implicit unlink of original file 
162
163         # Remove temporary file.
164         os.unlink(self.__newpath)
165
166         self.__state = self.__STATE['APPLIED']
167
168     def revert(self):
169         if self.__state != self.__STATE['APPLIED']:
170             raise Error("Attempt to revert configuration from state %s" % self.__state)
171
172         for child in self.__children:
173             child.revert()
174
175         log("Reverting changes to %s configuration" % self.__fname)
176
177         # Remove existing new configuration
178         if os.access(self.__newpath, os.F_OK):
179             os.unlink(self.__newpath)
180
181         # Revert new configuration.
182         if os.access(self.__path, os.F_OK):
183             os.link(self.__path, self.__newpath)
184             os.unlink(self.__path)
185
186         # Revert to old configuration.
187         if os.access(self.__oldpath, os.F_OK):
188             os.link(self.__oldpath, self.__path)
189             os.unlink(self.__oldpath)
190
191         # Leave .*.xapi-new as an aid to debugging.
192         
193         self.__state = self.__STATE['REVERTED']
194     
195     def commit(self):
196         if self.__state != self.__STATE['APPLIED']:
197             raise Error("Attempt to commit configuration from state %s" % self.__state)
198
199         for child in self.__children:
200             child.commit()
201
202         log("Committing changes to %s configuration" % self.__fname)
203         
204         if os.access(self.__oldpath, os.F_OK):
205             os.unlink(self.__oldpath)
206         if os.access(self.__newpath, os.F_OK):
207             os.unlink(self.__newpath)
208
209         self.__state = self.__STATE['COMMITTED']
210
211 def debug_mode():
212     return output_directory is not None
213
214 def log(s):
215     if debug_mode():
216         print >>sys.stderr, s
217     else:
218         syslog.syslog(s)
219
220 def check_allowed(pif):
221     pifrec = db.get_pif_record(pif)
222     try:
223         f = open("/proc/ardence")
224         macline = filter(lambda x: x.startswith("HWaddr:"), f.readlines())
225         f.close()
226         if len(macline) == 1:
227             p = re.compile(".*\s%(MAC)s\s.*" % pifrec, re.IGNORECASE)
228             if p.match(macline[0]):
229                 log("Skipping PVS device %(device)s (%(MAC)s)" % pifrec)
230                 return False
231     except IOError:
232         pass
233     return True
234
235 def interface_exists(i):
236     return os.path.exists("/sys/class/net/" + i)
237
238 def get_netdev_mac(device):
239     try:
240         return read_first_line_of_file("/sys/class/net/%s/address" % device)
241     except:
242         # Probably no such device.
243         return None
244
245 def get_netdev_tx_queue_len(device):
246     try:
247         return int(read_first_line_of_file("/sys/class/net/%s/tx_queue_len"
248                                            % device))
249     except:
250         # Probably no such device.
251         return None
252
253 def get_netdev_by_mac(mac):
254     for device in os.listdir("/sys/class/net"):
255         dev_mac = get_netdev_mac(device)
256         if (dev_mac and mac.lower() == dev_mac.lower() and
257             get_netdev_tx_queue_len(device)):
258                 return device
259     return None
260
261 #
262 # Helper functions for encoding/decoding database attributes to/from XML.
263 #
264 def str_to_xml(xml, parent, tag, val):
265     e = xml.createElement(tag)
266     parent.appendChild(e)
267     v = xml.createTextNode(val)
268     e.appendChild(v)
269 def str_from_xml(n):
270     def getText(nodelist):
271         rc = ""
272         for node in nodelist:
273             if node.nodeType == node.TEXT_NODE:
274                 rc = rc + node.data
275         return rc
276     return getText(n.childNodes).strip()
277
278
279 def bool_to_xml(xml, parent, tag, val):
280     if val:
281         str_to_xml(xml, parent, tag, "True")
282     else:
283         str_to_xml(xml, parent, tag, "False")
284 def bool_from_xml(n):
285     s = str_from_xml(n)
286     if s == "True":
287         return True
288     elif s == "False":
289         return False
290     else:
291         raise Error("Unknown boolean value %s" % s);
292
293 def strlist_to_xml(xml, parent, ltag, itag, val):
294     e = xml.createElement(ltag)
295     parent.appendChild(e)
296     for v in val:
297         c = xml.createElement(itag)
298         e.appendChild(c)
299         cv = xml.createTextNode(v)
300         c.appendChild(cv)
301 def strlist_from_xml(n, ltag, itag):
302     ret = []
303     for n in n.childNodes:
304         if n.nodeName == itag:
305             ret.append(str_from_xml(n))
306     return ret
307
308 def otherconfig_to_xml(xml, parent, val, attrs):
309     otherconfig = xml.createElement("other_config")
310     parent.appendChild(otherconfig)
311     for n,v in val.items():
312         if not n in attrs:
313             raise Error("Unknown other-config attribute: %s" % n)
314         str_to_xml(xml, otherconfig, n, v)
315 def otherconfig_from_xml(n, attrs):
316     ret = {}
317     for n in n.childNodes:
318         if n.nodeName in attrs:
319             ret[n.nodeName] = str_from_xml(n)
320     return ret
321
322 #
323 # Definitions of the database objects (and their attributes) used by interface-reconfigure.
324 #
325 # Each object is defined by a dictionary mapping an attribute name in
326 # the xapi database to a tuple containing two items:
327 #  - a function which takes this attribute and encodes it as XML.
328 #  - a function which takes XML and decocdes it into a value.
329 #
330 # other-config attributes are specified as a simple array of strings
331
332 PIF_XML_TAG = "pif"
333 VLAN_XML_TAG = "vlan"
334 BOND_XML_TAG = "bond"
335 NETWORK_XML_TAG = "network"
336
337 ETHTOOL_OTHERCONFIG_ATTRS = ['ethtool-%s' % x for x in 'autoneg', 'speed', 'duplex', 'rx', 'tx', 'sg', 'tso', 'ufo', 'gso' ]
338
339 PIF_ATTRS = { 'uuid': (str_to_xml,str_from_xml),
340               'management': (bool_to_xml,bool_from_xml),
341               'network': (str_to_xml,str_from_xml),
342               'device': (str_to_xml,str_from_xml),
343               'bond_master_of': (lambda x, p, t, v: strlist_to_xml(x, p, 'bond_master_of', 'slave', v),
344                                  lambda n: strlist_from_xml(n, 'bond_master_of', 'slave')),
345               'bond_slave_of': (str_to_xml,str_from_xml),
346               'VLAN': (str_to_xml,str_from_xml),
347               'VLAN_master_of': (str_to_xml,str_from_xml),
348               'VLAN_slave_of': (lambda x, p, t, v: strlist_to_xml(x, p, 'VLAN_slave_of', 'master', v),
349                                 lambda n: strlist_from_xml(n, 'VLAN_slave_Of', 'master')),
350               'ip_configuration_mode': (str_to_xml,str_from_xml),
351               'IP': (str_to_xml,str_from_xml),
352               'netmask': (str_to_xml,str_from_xml),
353               'gateway': (str_to_xml,str_from_xml),
354               'DNS': (str_to_xml,str_from_xml),
355               'MAC': (str_to_xml,str_from_xml),
356               'other_config': (lambda x, p, t, v: otherconfig_to_xml(x, p, v, PIF_OTHERCONFIG_ATTRS),
357                                lambda n: otherconfig_from_xml(n, PIF_OTHERCONFIG_ATTRS)),
358               
359               # Special case: We write the current value
360               # PIF.currently-attached to the cache but since it will
361               # not be valid when we come to use the cache later
362               # (i.e. after a reboot) we always read it as False.
363               'currently_attached': (bool_to_xml, lambda n: False),
364             }
365
366 PIF_OTHERCONFIG_ATTRS = [ 'domain', 'peerdns', 'defaultroute', 'mtu', 'static-routes' ] + \
367                         [ 'bond-%s' % x for x in 'mode', 'miimon', 'downdelay', 'updelay', 'use_carrier' ] + \
368                         ETHTOOL_OTHERCONFIG_ATTRS
369
370 VLAN_ATTRS = { 'uuid': (str_to_xml,str_from_xml),
371                'tagged_PIF': (str_to_xml,str_from_xml),
372                'untagged_PIF': (str_to_xml,str_from_xml),
373              }
374     
375 BOND_ATTRS = { 'uuid': (str_to_xml,str_from_xml),
376                'master': (str_to_xml,str_from_xml),
377                'slaves': (lambda x, p, t, v: strlist_to_xml(x, p, 'slaves', 'slave', v),
378                           lambda n: strlist_from_xml(n, 'slaves', 'slave')),
379              }
380
381 NETWORK_ATTRS = { 'uuid': (str_to_xml,str_from_xml),
382                   'bridge': (str_to_xml,str_from_xml),
383                   'PIFs': (lambda x, p, t, v: strlist_to_xml(x, p, 'PIFs', 'PIF', v),
384                            lambda n: strlist_from_xml(n, 'PIFs', 'PIF')),
385                   'other_config': (lambda x, p, t, v: otherconfig_to_xml(x, p, v, NETWORK_OTHERCONFIG_ATTRS),
386                                    lambda n: otherconfig_from_xml(n, NETWORK_OTHERCONFIG_ATTRS)),
387                 }
388
389 NETWORK_OTHERCONFIG_ATTRS = [ 'mtu', 'static-routes' ] + ETHTOOL_OTHERCONFIG_ATTRS
390
391 class DatabaseCache(object):
392     def __read_xensource_inventory(self):
393         filename = "/etc/xensource-inventory"
394         f = open(filename, "r")
395         lines = [x.strip("\n") for x in f.readlines()]
396         f.close()
397
398         defs = [ (l[:l.find("=")], l[(l.find("=") + 1):]) for l in lines ]
399         defs = [ (a, b.strip("'")) for (a,b) in defs ]
400
401         return dict(defs)
402     def __pif_on_host(self,pif):
403         return self.__pifs.has_key(pif)
404
405     def __get_pif_records_from_xapi(self, session, host):
406         self.__pifs = {}
407         for (p,rec) in session.xenapi.PIF.get_all_records().items():
408             if rec['host'] != host:
409                 continue
410             self.__pifs[p] = {}
411             for f in PIF_ATTRS:
412                 self.__pifs[p][f] = rec[f]
413             self.__pifs[p]['other_config'] = {}
414             for f in PIF_OTHERCONFIG_ATTRS:
415                 if not rec['other_config'].has_key(f): continue
416                 self.__pifs[p]['other_config'][f] = rec['other_config'][f]
417
418     def __get_vlan_records_from_xapi(self, session):
419         self.__vlans = {}
420         for v in session.xenapi.VLAN.get_all():
421             rec = session.xenapi.VLAN.get_record(v)
422             if not self.__pif_on_host(rec['untagged_PIF']):
423                 continue
424             self.__vlans[v] = {}
425             for f in VLAN_ATTRS:
426                 self.__vlans[v][f] = rec[f]
427
428     def __get_bond_records_from_xapi(self, session):
429         self.__bonds = {}
430         for b in session.xenapi.Bond.get_all():
431             rec = session.xenapi.Bond.get_record(b)
432             if not self.__pif_on_host(rec['master']):
433                 continue
434             self.__bonds[b] = {}
435             for f in BOND_ATTRS:
436                 self.__bonds[b][f] = rec[f]
437
438     def __get_network_records_from_xapi(self, session):
439         self.__networks = {}
440         for n in session.xenapi.network.get_all():
441             rec = session.xenapi.network.get_record(n)
442             self.__networks[n] = {}
443             for f in NETWORK_ATTRS:
444                 if f == "PIFs":
445                     # drop PIFs on other hosts
446                     self.__networks[n][f] = [p for p in rec[f] if self.__pif_on_host(p)]
447                 else:
448                     self.__networks[n][f] = rec[f]
449             self.__networks[n]['other_config'] = {}
450             for f in NETWORK_OTHERCONFIG_ATTRS:
451                 if not rec['other_config'].has_key(f): continue
452                 self.__networks[n]['other_config'][f] = rec['other_config'][f]
453
454     def __to_xml(self, xml, parent, key, ref, rec, attrs):
455         """Encode a database object as XML"""
456         e = xml.createElement(key)
457         parent.appendChild(e)
458         if ref:
459             e.setAttribute('ref', ref)
460
461         for n,v in rec.items():
462             if attrs.has_key(n):
463                 h,_ = attrs[n]
464                 h(xml, e, n, v)
465             else:
466                 raise Error("Unknown attribute %s" % n)
467     def __from_xml(self, e, attrs):
468         """Decode a database object from XML"""
469         ref = e.attributes['ref'].value
470         rec = {}
471         for n in e.childNodes:
472             if n.nodeName in attrs:
473                 _,h = attrs[n.nodeName]
474                 rec[n.nodeName] = h(n)
475         return (ref,rec)
476     
477     def __init__(self, session_ref=None, cache_file=None):
478         if session_ref and cache_file:
479             raise Error("can't specify session reference and cache file")
480         if cache_file == None:
481             session = XenAPI.xapi_local()
482
483             if not session_ref:
484                 log("No session ref given on command line, logging in.")
485                 session.xenapi.login_with_password("root", "")
486             else:
487                 session._session = session_ref
488
489             try:
490                 
491                 inventory = self.__read_xensource_inventory()
492                 assert(inventory.has_key('INSTALLATION_UUID'))
493                 log("host uuid is %s" % inventory['INSTALLATION_UUID'])
494                 
495                 host = session.xenapi.host.get_by_uuid(inventory['INSTALLATION_UUID'])
496                 
497                 self.__get_pif_records_from_xapi(session, host)
498
499                 self.__get_vlan_records_from_xapi(session)
500                 self.__get_bond_records_from_xapi(session)
501                 self.__get_network_records_from_xapi(session)
502             finally:
503                 if not session_ref:
504                     session.xenapi.session.logout()
505         else:
506             log("Loading xapi database cache from %s" % cache_file)
507
508             xml = parseXML(cache_file)
509
510             self.__pifs = {}
511             self.__bonds = {}
512             self.__vlans = {}
513             self.__networks = {}
514
515             assert(len(xml.childNodes) == 1)
516             toplevel = xml.childNodes[0]
517             
518             assert(toplevel.nodeName == "xenserver-network-configuration")
519             
520             for n in toplevel.childNodes:
521                 if n.nodeName == "#text":
522                     pass
523                 elif n.nodeName == PIF_XML_TAG:
524                     (ref,rec) = self.__from_xml(n, PIF_ATTRS)
525                     self.__pifs[ref] = rec
526                 elif n.nodeName == BOND_XML_TAG:
527                     (ref,rec) = self.__from_xml(n, BOND_ATTRS)
528                     self.__bonds[ref] = rec
529                 elif n.nodeName == VLAN_XML_TAG:
530                     (ref,rec) = self.__from_xml(n, VLAN_ATTRS)
531                     self.__vlans[ref] = rec
532                 elif n.nodeName == NETWORK_XML_TAG:
533                     (ref,rec) = self.__from_xml(n, NETWORK_ATTRS)
534                     self.__networks[ref] = rec
535                 else:
536                     raise Error("Unknown XML element %s" % n.nodeName)
537
538     def save(self, cache_file):
539
540         xml = getDOMImplementation().createDocument(
541             None, "xenserver-network-configuration", None)
542         for (ref,rec) in self.__pifs.items():
543             self.__to_xml(xml, xml.documentElement, PIF_XML_TAG, ref, rec, PIF_ATTRS)
544         for (ref,rec) in self.__bonds.items():
545             self.__to_xml(xml, xml.documentElement, BOND_XML_TAG, ref, rec, BOND_ATTRS)
546         for (ref,rec) in self.__vlans.items():
547             self.__to_xml(xml, xml.documentElement, VLAN_XML_TAG, ref, rec, VLAN_ATTRS)
548         for (ref,rec) in self.__networks.items():
549             self.__to_xml(xml, xml.documentElement, NETWORK_XML_TAG, ref, rec,
550                           NETWORK_ATTRS)
551             
552         f = open(cache_file, 'w')
553         f.write(xml.toprettyxml())
554         f.close()
555
556     def get_pif_by_uuid(self, uuid):
557         pifs = map(lambda (ref,rec): ref,
558                   filter(lambda (ref,rec): uuid == rec['uuid'],
559                          self.__pifs.items()))
560         if len(pifs) == 0:
561             raise Error("Unknown PIF \"%s\"" % uuid)
562         elif len(pifs) > 1:
563             raise Error("Non-unique PIF \"%s\"" % uuid)
564
565         return pifs[0]
566
567     def get_pifs_by_device(self, device):
568         return map(lambda (ref,rec): ref,
569                    filter(lambda (ref,rec): rec['device'] == device,
570                           self.__pifs.items()))
571
572     def get_pif_by_bridge(self, bridge):
573         networks = map(lambda (ref,rec): ref,
574                        filter(lambda (ref,rec): rec['bridge'] == bridge,
575                               self.__networks.items()))
576         if len(networks) == 0:
577             raise Error("No matching network \"%s\"")
578
579         answer = None
580         for network in networks:
581             nwrec = self.get_network_record(network)
582             for pif in nwrec['PIFs']:
583                 pifrec = self.get_pif_record(pif)
584                 if answer:
585                     raise Error("Multiple PIFs on host for network %s" % (bridge))
586                 answer = pif
587         if not answer:
588             raise Error("No PIF on host for network %s" % (bridge))
589         return answer
590
591     def get_pif_record(self, pif):
592         if self.__pifs.has_key(pif):
593             return self.__pifs[pif]
594         raise Error("Unknown PIF \"%s\" (get_pif_record)" % pif)
595     def get_all_pifs(self):
596         return self.__pifs
597     def pif_exists(self, pif):
598         return self.__pifs.has_key(pif)
599     
600     def get_management_pif(self):
601         """ Returns the management pif on host
602         """
603         all = self.get_all_pifs()
604         for pif in all: 
605             pifrec = self.get_pif_record(pif)
606             if pifrec['management']: return pif
607         return None
608
609     def get_network_record(self, network):
610         if self.__networks.has_key(network):
611             return self.__networks[network]
612         raise Error("Unknown network \"%s\"" % network)
613     def get_all_networks(self):
614         return self.__networks
615
616     def get_bond_record(self, bond):
617         if self.__bonds.has_key(bond):
618             return self.__bonds[bond]
619         else:
620             return None
621         
622     def get_vlan_record(self, vlan):
623         if self.__vlans.has_key(vlan):
624             return self.__vlans[vlan]
625         else:
626             return None
627             
628 def bridge_name(pif):
629     """Return the bridge name associated with pif, or None if network is bridgeless"""
630     pifrec = db.get_pif_record(pif)
631     nwrec = db.get_network_record(pifrec['network'])
632
633     if nwrec['bridge']:
634         # TODO: sanity check that nwrec['bridgeless'] != 'true'
635         return nwrec['bridge']
636     else:
637         # TODO: sanity check that nwrec['bridgeless'] == 'true'
638         return None
639
640 def interface_name(pif):
641     """Construct an interface name from the given PIF record."""
642
643     pifrec = db.get_pif_record(pif)
644
645     if pifrec['VLAN'] == '-1':
646         return pifrec['device']
647     else:
648         return "%(device)s.%(VLAN)s" % pifrec
649
650 def datapath_name(pif):
651     """Return the OpenFlow datapath name associated with pif.
652 For a non-VLAN PIF, the datapath name is the bridge name.
653 For a VLAN PIF, the datapath name is the bridge name for the PIF's VLAN slave.
654 (xapi will create a datapath named with the bridge name even though we won't
655 use it.)
656 """
657
658
659     pifrec = db.get_pif_record(pif)
660
661     if pifrec['VLAN'] == '-1':
662         return bridge_name(pif)
663     else:
664         return bridge_name(get_vlan_slave_of_pif(pif))
665
666 def ipdev_name(pif):
667     """Return the the name of the network device that carries the
668 IP configuration (if any) associated with pif.
669 The ipdev name is the same as the bridge name.
670 """
671
672     pifrec = db.get_pif_record(pif)
673     return bridge_name(pif)
674
675 def get_physdev_pifs(pif):
676     """Return the PIFs for the physical network device(s) associated with pif.
677 For a VLAN PIF, this is the VLAN slave's physical device PIF.
678 For a bond master PIF, these are the bond slave PIFs.
679 For a non-VLAN, non-bond master PIF, the PIF is its own physical device PIF.
680 """
681     pifrec = db.get_pif_record(pif)
682
683     if pifrec['VLAN'] != '-1':
684         return get_physdev_pifs(get_vlan_slave_of_pif(pif))
685     elif len(pifrec['bond_master_of']) != 0:
686         return get_bond_slaves_of_pif(pif)
687     else:
688         return [pif]
689
690 def get_physdev_names(pif):
691     """Return the name(s) of the physical network device(s) associated with pif.
692 For a VLAN PIF, the physical devices are the VLAN slave's physical devices.
693 For a bond master PIF, the physical devices are the bond slaves.
694 For a non-VLAN, non-bond master PIF, the physical device is the PIF itself.
695 """
696
697     return [db.get_pif_record(phys)['device'] for phys in get_physdev_pifs(pif)]
698
699 def log_pif_action(action, pif):
700     pifrec = db.get_pif_record(pif)
701     rec = {}
702     rec['uuid'] = pifrec['uuid']
703     rec['ip_configuration_mode'] = pifrec['ip_configuration_mode']
704     rec['action'] = action
705     rec['interface-name'] = interface_name(pif)
706     rec['message'] = "Bring %(action)s PIF %(uuid)s" % rec
707     log("%(message)s: %(interface-name)s configured as %(ip_configuration_mode)s" % rec)
708
709 def get_bond_masters_of_pif(pif):
710     """Returns a list of PIFs which are bond masters of this PIF"""
711
712     pifrec = db.get_pif_record(pif)
713
714     bso = pifrec['bond_slave_of']
715
716     # bond-slave-of is currently a single reference but in principle a
717     # PIF could be a member of several bonds which are not
718     # concurrently attached. Be robust to this possibility.
719     if not bso or bso == "OpaqueRef:NULL":
720         bso = []
721     elif not type(bso) == list:
722         bso = [bso]
723
724     bondrecs = [db.get_bond_record(bond) for bond in bso]
725     bondrecs = [rec for rec in bondrecs if rec]
726
727     return [bond['master'] for bond in bondrecs]
728
729 def get_bond_slaves_of_pif(pif):
730     """Returns a list of PIFs which make up the given bonded pif."""
731     
732     pifrec = db.get_pif_record(pif)
733
734     bmo = pifrec['bond_master_of']
735     if len(bmo) > 1:
736         raise Error("Bond-master-of contains too many elements")
737     
738     if len(bmo) == 0:
739         return []
740     
741     bondrec = db.get_bond_record(bmo[0])
742     if not bondrec:
743         raise Error("No bond record for bond master PIF")
744
745     return bondrec['slaves']
746
747 def get_vlan_slave_of_pif(pif):
748     """Find the PIF which is the VLAN slave of pif.
749
750 Returns the 'physical' PIF underneath the a VLAN PIF @pif."""
751     
752     pifrec = db.get_pif_record(pif)
753
754     vlan = pifrec['VLAN_master_of']
755     if not vlan or vlan == "OpaqueRef:NULL":
756         raise Error("PIF is not a VLAN master")
757
758     vlanrec = db.get_vlan_record(vlan)
759     if not vlanrec:
760         raise Error("No VLAN record found for PIF")
761
762     return vlanrec['tagged_PIF']
763
764 def get_vlan_masters_of_pif(pif):
765     """Returns a list of PIFs which are VLANs on top of the given pif."""
766     
767     pifrec = db.get_pif_record(pif)
768     vlans = [db.get_vlan_record(v) for v in pifrec['VLAN_slave_of']]
769     return [v['untagged_PIF'] for v in vlans if v and db.pif_exists(v['untagged_PIF'])]
770
771 def interface_deconfigure_commands(interface):
772     # The use of [!0-9] keeps an interface of 'eth0' from matching
773     # VLANs attached to eth0 (such as 'eth0.123'), which are distinct
774     # interfaces.
775     return ['--del-match=bridge.*.port=%s' % interface,
776             '--del-match=bonding.%s.[!0-9]*' % interface,
777             '--del-match=bonding.*.slave=%s' % interface,
778             '--del-match=vlan.%s.[!0-9]*' % interface,
779             '--del-match=port.%s.[!0-9]*' % interface,
780             '--del-match=iface.%s.[!0-9]*' % interface]
781
782 def run_command(command):
783     log("Running command: " + ' '.join(command))
784     if os.spawnl(os.P_WAIT, command[0], *command) != 0:
785         log("Command failed: " + ' '.join(command))
786         return False
787     return True
788
789 def rename_netdev(old_name, new_name):
790     log("Changing the name of %s to %s" % (old_name, new_name))
791     run_command(['/sbin/ifconfig', old_name, 'down'])
792     if not run_command(['/sbin/ip', 'link', 'set', old_name,
793                         'name', new_name]):
794         raise Error("Could not rename %s to %s" % (old_name, new_name))
795
796 # Check whether 'pif' exists and has the correct MAC.
797 # If not, try to find a device with the correct MAC and rename it.
798 # 'already_renamed' is used to avoid infinite recursion.
799 def remap_pif(pif, already_renamed=[]):
800     pifrec = db.get_pif_record(pif)
801     device = pifrec['device']
802     mac = pifrec['MAC']
803
804     # Is there a network device named 'device' at all?
805     device_exists = interface_exists(device)
806     if device_exists:
807         # Yes.  Does it have MAC 'mac'?
808         found_mac = get_netdev_mac(device)
809         if found_mac and mac.lower() == found_mac.lower():
810             # Yes, everything checks out the way we want.  Nothing to do.
811             return
812     else:
813         log("No network device %s" % device)
814
815     # What device has MAC 'mac'?
816     cur_device = get_netdev_by_mac(mac)
817     if not cur_device:
818         log("No network device has MAC %s" % mac)
819         return
820
821     # First rename 'device', if it exists, to get it out of the way
822     # for 'cur_device' to replace it.
823     if device_exists:
824         rename_netdev(device, "dev%d" % random.getrandbits(24))
825
826     # Rename 'cur_device' to 'device'.
827     rename_netdev(cur_device, device)
828
829 def read_first_line_of_file(name):
830     file = None
831     try:
832         file = open(name, 'r')
833         return file.readline().rstrip('\n')
834     finally:
835         if file != None:
836             file.close()
837
838 def down_netdev(interface, deconfigure=True):
839     if not interface_exists(interface):
840         log("down_netdev: interface %s does not exist, ignoring" % interface)
841         return
842     if deconfigure:
843         # Kill dhclient.
844         pidfile_name = '/var/run/dhclient-%s.pid' % interface
845         try:
846             os.kill(int(read_first_line_of_file(pidfile_name)), signal.SIGTERM)
847         except:
848             pass
849
850         # Remove dhclient pidfile.
851         try:
852             os.remove(pidfile_name)
853         except:
854             pass
855         
856         run_command(["/sbin/ifconfig", interface, '0.0.0.0'])
857
858     run_command(["/sbin/ifconfig", interface, 'down'])
859
860 def up_netdev(interface):
861     run_command(["/sbin/ifconfig", interface, 'up'])
862
863 def find_distinguished_pifs(pif):
864     """Returns the PIFs on host that own DNS and the default route.
865 The peerdns pif will be the one with pif::other-config:peerdns=true, or the mgmt pif if none have this set.
866 The gateway pif will be the one with pif::other-config:defaultroute=true, or the mgmt pif if none have this set.
867
868 Note: we prune out the bond master pif (if it exists).
869 This is because when we are called to bring up an interface with a bond master, it is implicit that
870 we should bring down that master."""
871
872     pifrec = db.get_pif_record(pif)
873
874     pifs = [ __pif for __pif in db.get_all_pifs() if
875                      (not  __pif in get_bond_masters_of_pif(pif)) ]
876
877     peerdns_pif = None
878     defaultroute_pif = None
879     
880     # loop through all the pifs on this host looking for one with
881     #   other-config:peerdns = true, and one with
882     #   other-config:default-route=true
883     for __pif in pifs:
884         __pifrec = db.get_pif_record(__pif)
885         __oc = __pifrec['other_config']
886         if __oc.has_key('peerdns') and __oc['peerdns'] == 'true':
887             if peerdns_pif == None:
888                 peerdns_pif = __pif
889             else:
890                 log('Warning: multiple pifs with "peerdns=true" - choosing %s and ignoring %s' % \
891                         (db.get_pif_record(peerdns_pif)['device'], __pifrec['device']))
892         if __oc.has_key('defaultroute') and __oc['defaultroute'] == 'true':
893             if defaultroute_pif == None:
894                 defaultroute_pif = __pif
895             else:
896                 log('Warning: multiple pifs with "defaultroute=true" - choosing %s and ignoring %s' % \
897                         (db.get_pif_record(defaultroute_pif)['device'], __pifrec['device']))
898     
899     # If no pif is explicitly specified then use the mgmt pif for peerdns/defaultroute
900     if peerdns_pif == None:
901         peerdns_pif = management_pif
902     if defaultroute_pif == None:
903         defaultroute_pif = management_pif
904
905     return peerdns_pif, defaultroute_pif
906
907 def run_ethtool(device, oc):
908     # Run "ethtool -s" if there are any settings.
909     settings = []
910     if oc.has_key('ethtool-speed'):
911         val = oc['ethtool-speed']
912         if val in ["10", "100", "1000"]:
913             settings += ['speed', val]
914         else:
915             log("Invalid value for ethtool-speed = %s. Must be 10|100|1000." % val)
916     if oc.has_key('ethtool-duplex'):
917         val = oc['ethtool-duplex']
918         if val in ["10", "100", "1000"]:
919             settings += ['duplex', 'val']
920         else:
921             log("Invalid value for ethtool-duplex = %s. Must be half|full." % val)
922     if oc.has_key('ethtool-autoneg'):
923         val = oc['ethtool-autoneg']
924         if val in ["true", "on"]:
925             settings += ['autoneg', 'on']
926         elif val in ["false", "off"]:
927             settings += ['autoneg', 'off']
928         else:
929             log("Invalid value for ethtool-autoneg = %s. Must be on|true|off|false." % val)
930     if settings:
931         run_command(['/sbin/ethtool', '-s', device] + settings)
932
933     # Run "ethtool -K" if there are any offload settings.
934     offload = []
935     for opt in ("rx", "tx", "sg", "tso", "ufo", "gso"):
936         if oc.has_key("ethtool-" + opt):
937             val = oc["ethtool-" + opt]
938             if val in ["true", "on"]:
939                 offload += [opt, 'on']
940             elif val in ["false", "off"]:
941                 offload += [opt, 'off']
942             else:
943                 log("Invalid value for ethtool-%s = %s. Must be on|true|off|false." % (opt, val))
944     if offload:
945         run_command(['/sbin/ethtool', '-K', device] + offload)
946
947 def mtu_setting(oc):
948     if oc.has_key('mtu'):
949         try:
950             int(oc['mtu'])      # Check that the value is an integer
951             return ['mtu', oc['mtu']]
952         except ValueError, x:
953             log("Invalid value for mtu = %s" % mtu)
954     return []
955
956 def configure_local_port(pif):
957     pifrec = db.get_pif_record(pif)
958     datapath = datapath_name(pif)
959     ipdev = ipdev_name(pif)
960
961     nw = pifrec['network']
962     nwrec = db.get_network_record(nw)
963
964     pif_oc = pifrec['other_config']
965     nw_oc = nwrec['other_config']
966
967     # IP (except DHCP) and MTU.
968     ifconfig_argv = ['/sbin/ifconfig', ipdev, 'up']
969     gateway = ''
970     if pifrec['ip_configuration_mode'] == "DHCP":
971         pass
972     elif pifrec['ip_configuration_mode'] == "Static":
973         ifconfig_argv += [pifrec['IP']]
974         ifconfig_argv += ['netmask', pifrec['netmask']]
975         gateway = pifrec['gateway']
976     elif pifrec['ip_configuration_mode'] == "None":
977         # Nothing to do.
978         pass
979     else:
980         raise Error("Unknown IP-configuration-mode %s" % pifrec['ip_configuration_mode'])
981     ifconfig_argv += mtu_setting(nw_oc)
982     run_command(ifconfig_argv)
983     
984     (peerdns_pif, defaultroute_pif) = find_distinguished_pifs(pif)
985
986     # /etc/resolv.conf
987     if peerdns_pif == pif:
988         f = ConfigurationFile('resolv.conf', "/etc")
989         if pif_oc.has_key('domain'):
990             f.write("search %s\n" % pif_oc['domain'])
991         for dns in pifrec['DNS'].split(","): 
992             f.write("nameserver %s\n" % dns)
993         f.close()
994         f.apply()
995         f.commit()
996
997     # Routing.
998     if defaultroute_pif == pif and gateway != '':
999         run_command(['/sbin/ip', 'route', 'replace', 'default',
1000                      'via', gateway, 'dev', ipdev])
1001     if nw_oc.has_key('static-routes'):
1002         for line in nw_oc['static-routes'].split(','):
1003             network, masklen, gateway = line.split('/')
1004             run_command(['/sbin/ip', 'route', 'add',
1005                          '%s/%s' % (network, masklen), 'via', gateway,
1006                          'dev', ipdev])
1007
1008     # Ethtool.
1009     run_ethtool(ipdev, nw_oc)
1010
1011     # DHCP.
1012     if pifrec['ip_configuration_mode'] == "DHCP":
1013         print
1014         print "Determining IP information for %s..." % ipdev,
1015         argv = ['/sbin/dhclient', '-q',
1016                 '-lf', '/var/lib/dhclient/dhclient-%s.leases' % ipdev,
1017                 '-pf', '/var/run/dhclient-%s.pid' % ipdev,
1018                 ipdev]
1019         if run_command(argv):
1020             print 'done.'
1021         else:
1022             print 'failed.'
1023
1024 def configure_physdev(pif):
1025     pifrec = db.get_pif_record(pif)
1026     device = pifrec['device']
1027     oc = pifrec['other_config']
1028
1029     run_command(['/sbin/ifconfig', device, 'up'] + mtu_setting(oc))
1030     run_ethtool(device, oc)
1031
1032 def modify_config(commands):
1033     run_command(['/usr/bin/ovs-cfg-mod', '-vANY:console:emer',
1034                  '-F', '/etc/ovs-vswitchd.conf']
1035                 + commands + ['-c'])
1036     run_command(['/sbin/service', 'vswitch', 'reload'])
1037
1038 def is_bond_pif(pif):
1039     pifrec = db.get_pif_record(pif)
1040     return len(pifrec['bond_master_of']) != 0
1041
1042 def configure_bond(pif):
1043     pifrec = db.get_pif_record(pif)
1044     interface = interface_name(pif)
1045     ipdev = ipdev_name(pif)
1046     datapath = datapath_name(pif)
1047     physdev_names = get_physdev_names(pif)
1048
1049     argv = ['--del-match=bonding.%s.[!0-9]*' % interface]
1050     argv += ["--add=bonding.%s.slave=%s" % (interface, slave)
1051              for slave in physdev_names]
1052     argv += ['--add=bonding.%s.fake-iface=true' % interface]
1053
1054     if pifrec['MAC'] != "":
1055         argv += ['--add=port.%s.mac=%s' % (interface, pifrec['MAC'])]
1056
1057     # Bonding options.
1058     bond_options = { 
1059         "mode":   "balance-slb",
1060         "miimon": "100",
1061         "downdelay": "200",
1062         "updelay": "31000",
1063         "use_carrier": "1",
1064         }
1065     # override defaults with values from other-config whose keys
1066     # being with "bond-"
1067     oc = pifrec['other_config']
1068     overrides = filter(lambda (key,val):
1069                            key.startswith("bond-"), oc.items())
1070     overrides = map(lambda (key,val): (key[5:], val), overrides)
1071     bond_options.update(overrides)
1072     for (name,val) in bond_options.items():
1073         argv += ["--add=bonding.%s.%s=%s" % (interface, name, val)]
1074     return argv
1075
1076 def action_up(pif):
1077     pifrec = db.get_pif_record(pif)
1078
1079     bridge = bridge_name(pif)
1080     interface = interface_name(pif)
1081     ipdev = ipdev_name(pif)
1082     datapath = datapath_name(pif)
1083     physdev_pifs = get_physdev_pifs(pif)
1084     physdev_names = get_physdev_names(pif)
1085     vlan_slave = None
1086     if pifrec['VLAN'] != '-1':
1087         vlan_slave = get_vlan_slave_of_pif(pif)
1088     if vlan_slave and is_bond_pif(vlan_slave):
1089         bond_master = vlan_slave
1090     elif is_bond_pif(pif):
1091         bond_master = pif
1092     else:
1093         bond_master = None
1094     if bond_master:
1095         bond_slaves = get_bond_slaves_of_pif(bond_master)
1096     else:
1097         bond_slaves = []
1098     bond_masters = get_bond_masters_of_pif(pif)
1099
1100     # Support "rpm -e vswitch" gracefully by keeping Centos configuration
1101     # files up-to-date, even though we don't use them or need them.
1102     f = configure_pif(pif)
1103     mode = pifrec['ip_configuration_mode']
1104     if bridge:
1105         log("Configuring %s using %s configuration" % (bridge, mode))
1106         br = open_network_ifcfg(pif)
1107         configure_network(pif, br)
1108         br.close()
1109         f.attach_child(br)
1110     else:
1111         log("Configuring %s using %s configuration" % (interface, mode))
1112         configure_network(pif, f)
1113     f.close()
1114     for master in bond_masters:
1115         master_bridge = bridge_name(master)
1116         removed = unconfigure_pif(master)
1117         f.attach_child(removed)
1118         if master_bridge:
1119             removed = open_network_ifcfg(master)
1120             log("Unlinking stale file %s" % removed.path())
1121             removed.unlink()
1122             f.attach_child(removed)
1123
1124     # /etc/xensource/scripts/vif needs to know where to add VIFs.
1125     if vlan_slave:
1126         if not os.path.exists(vswitch_state_dir):
1127             os.mkdir(vswitch_state_dir)
1128         br = ConfigurationFile("br-%s" % bridge, vswitch_state_dir)
1129         br.write("VLAN_SLAVE=%s\n" % datapath)
1130         br.write("VLAN_VID=%s\n" % pifrec['VLAN'])
1131         br.close()
1132         f.attach_child(br)
1133
1134     # Update all configuration files (both ours and Centos's).
1135     f.apply()
1136     f.commit()
1137
1138     # Check the MAC address of each network device and remap if
1139     # necessary to make names match our expectations.
1140     for physdev_pif in physdev_pifs:
1141         remap_pif(physdev_pif)
1142
1143     # "ifconfig down" the network device and delete its IP address, etc.
1144     down_netdev(ipdev)
1145     for physdev_name in physdev_names:
1146         down_netdev(physdev_name)
1147
1148     # If we are bringing up a bond, remove IP addresses from the
1149     # slaves (because we are implicitly being asked to take them down).
1150     # 
1151     # Conversely, if we are bringing up an interface that has bond
1152     # masters, remove IP addresses from the bond master (because we
1153     # are implicitly being asked to take it down).
1154     for bond_pif in bond_slaves + bond_masters:
1155         run_command(["/sbin/ifconfig", ipdev_name(bond_pif), '0.0.0.0']) 
1156
1157     # Remove all keys related to pif and any bond masters linked to PIF.
1158     del_ports = [ipdev] + physdev_names + bond_masters
1159     if vlan_slave and bond_master:
1160         del_ports += [interface_name(bond_master)]
1161     
1162     # What ports do we need to add to the datapath?
1163     #
1164     # We definitely need the ipdev, and ordinarily we want the
1165     # physical devices too, but for bonds we need the bond as bridge
1166     # port.
1167     add_ports = [ipdev, datapath]
1168     if not bond_master:
1169         add_ports += physdev_names
1170     else:
1171         add_ports += [interface_name(bond_master)]
1172
1173     # What ports do we need to delete?
1174     #
1175     #  - All the ports that we add, to avoid duplication and to drop
1176     #    them from another datapath in case they're misassigned.
1177     #    
1178     #  - The physical devices, since they will either be in add_ports
1179     #    or added to the bonding device (see below).
1180     #
1181     #  - The bond masters for pif.  (Ordinarily pif shouldn't have any
1182     #    bond masters.  If it does then interface-reconfigure is
1183     #    implicitly being asked to take them down.)
1184     del_ports = add_ports + physdev_names + bond_masters
1185
1186     # What networks does this datapath carry?
1187     #
1188     # - The network corresponding to the datapath's PIF.
1189     #
1190     # - The networks corresponding to any VLANs attached to the
1191     #   datapath's PIF.
1192     network_uuids = []
1193     for nwpif in db.get_pifs_by_device(pifrec['device']):
1194         net = db.get_pif_record(nwpif)['network']
1195         network_uuids += [db.get_network_record(net)['uuid']]
1196
1197     # Bring up bond slaves early, because ovs-vswitchd initially
1198     # enables or disables bond slaves based on whether carrier is
1199     # detected when they are added, and a network device that is down
1200     # always reports "no carrier".
1201     bond_slave_physdev_pifs = []
1202     for slave in bond_slaves:
1203         bond_slave_physdev_pifs += get_physdev_pifs(slave)
1204     for slave_physdev_pif in set(bond_slave_physdev_pifs):
1205         configure_physdev(slave_physdev_pif)
1206
1207     # Now modify the ovs-vswitchd config file.
1208     argv = []
1209     for port in set(del_ports):
1210         argv += interface_deconfigure_commands(port)
1211     for port in set(add_ports):
1212         argv += ['--add=bridge.%s.port=%s' % (datapath, port)]
1213     if vlan_slave:
1214         argv += ['--add=vlan.%s.tag=%s' % (ipdev, pifrec['VLAN'])]
1215         argv += ['--add=iface.%s.internal=true' % (ipdev)]
1216
1217         # xapi creates a bridge by the name of the ipdev and requires
1218         # that the IP address will be on it.  We need to delete this
1219         # bridge because we need that device to be a member of our
1220         # datapath.
1221         argv += ['--del-match=bridge.%s.[!0-9]*' % ipdev]
1222
1223         # xapi insists that its attempts to create the bridge succeed,
1224         # so force that to happen.
1225         argv += ['--add=iface.%s.fake-bridge=true' % (ipdev)]
1226     else:
1227         try:
1228             os.unlink("%s/br-%s" % (vswitch_state_dir, bridge))
1229         except OSError:
1230             pass
1231     argv += ['--del-match=bridge.%s.xs-network-uuids=*' % datapath]
1232     argv += ['--add=bridge.%s.xs-network-uuids=%s' % (datapath, uuid)
1233              for uuid in set(network_uuids)]
1234     if bond_master:
1235         argv += configure_bond(bond_master)
1236     modify_config(argv)
1237
1238     # Bring up VLAN slave, plus physical devices other than bond
1239     # slaves (which we brought up earlier).
1240     # XXX need to bring up bond itself.
1241     # XXX need to set MAC address on fake bridge
1242     if vlan_slave:
1243         up_netdev(ipdev_name(vlan_slave))
1244     for physdev_pif in set(physdev_pifs) - set(bond_slave_physdev_pifs):
1245         configure_physdev(physdev_pif)
1246
1247     # Configure network device for local port.
1248     configure_local_port(pif)
1249
1250     # Update /etc/issue (which contains the IP address of the management interface)
1251     os.system("/sbin/update-issue")
1252
1253     if bond_slaves:
1254         # There seems to be a race somewhere: without this sleep, using
1255         # XenCenter to create a bond that becomes the management interface
1256         # fails with "The underlying connection was closed: A connection that
1257         # was expected to be kept alive was closed by the server." on every
1258         # second or third try, even though /var/log/messages doesn't show
1259         # anything unusual.
1260         #
1261         # The race is probably present even without vswitch, but bringing up a
1262         # bond without vswitch involves a built-in pause of 10 seconds or more
1263         # to wait for the bond to transition from learning to forwarding state.
1264         time.sleep(5)
1265         
1266 def action_down(pif):
1267     rec = db.get_pif_record(pif)    
1268     interface = interface_name(pif)
1269     bridge = bridge_name(pif)
1270     ipdev = ipdev_name(pif)
1271
1272     # Support "rpm -e vswitch" gracefully by keeping Centos configuration
1273     # files up-to-date, even though we don't use them or need them.
1274     f = unconfigure_pif(pif)
1275     if bridge:
1276         br = open_network_ifcfg(pif)
1277         log("Unlinking stale file %s" % br.path())
1278         br.unlink()
1279         f.attach_child(br)
1280     try:
1281         f.apply()
1282         f.commit()
1283     except Error, e:
1284         log("action_down failed to apply changes: %s" % e.msg)
1285         f.revert()
1286         raise
1287
1288     argv = []
1289     if rec['VLAN'] != '-1':
1290         # Get rid of the VLAN device itself.
1291         down_netdev(ipdev)
1292         argv += interface_deconfigure_commands(ipdev)
1293
1294         # If the VLAN's slave is attached, stop here.
1295         slave = get_vlan_slave_of_pif(pif)
1296         if db.get_pif_record(slave)['currently_attached']:
1297             log("VLAN slave is currently attached")
1298             modify_config(argv)
1299             return
1300         
1301         # If the VLAN's slave has other VLANs that are attached, stop here.
1302         masters = get_vlan_masters_of_pif(slave)
1303         for m in masters:
1304             if m != pif and db.get_pif_record(m)['currently_attached']:
1305                 log("VLAN slave has other master %s" % interface_naem(m))
1306                 modify_config(argv)
1307                 return
1308
1309         # Otherwise, take down the VLAN's slave too.
1310         log("No more masters, bring down vlan slave %s" % interface_name(slave))
1311         pif = slave
1312     else:
1313         # Stop here if this PIF has attached VLAN masters.
1314         vlan_masters = get_vlan_masters_of_pif(pif)
1315         log("VLAN masters of %s - %s" % (rec['device'], [interface_name(m) for m in vlan_masters]))
1316         for m in vlan_masters:
1317             if db.get_pif_record(m)['currently_attached']:
1318                 # XXX remove IP address
1319                 log("Leaving %s up due to currently attached VLAN master %s" % (interface, interface_name(m)))
1320                 return
1321
1322     # pif is now either a bond or a physical device which needs to be
1323     # brought down.  pif might have changed so re-check all its attributes.
1324     rec = db.get_pif_record(pif)
1325     interface = interface_name(pif)
1326     bridge = bridge_name(pif)
1327     ipdev = ipdev_name(pif)
1328
1329
1330     bond_slaves = get_bond_slaves_of_pif(pif)
1331     log("bond slaves of %s - %s" % (rec['device'], [interface_name(s) for s in bond_slaves]))
1332     for slave in bond_slaves:
1333         slave_interface = interface_name(slave)
1334         log("bring down bond slave %s" % slave_interface)
1335         argv += interface_deconfigure_commands(slave_interface)
1336         down_netdev(slave_interface)
1337
1338     argv += interface_deconfigure_commands(ipdev)
1339     down_netdev(ipdev)
1340
1341     argv += ['--del-match', 'bridge.%s.*' % datapath_name(pif)]
1342     argv += ['--del-match', 'bonding.%s.[!0-9]*' % interface]
1343     modify_config(argv)
1344
1345 def action_rewrite(pif):
1346     # Support "rpm -e vswitch" gracefully by keeping Centos configuration
1347     # files up-to-date, even though we don't use them or need them.
1348     pifrec = db.get_pif_record(pif)
1349     f = configure_pif(pif)
1350     interface = interface_name(pif)
1351     bridge = bridge_name(pif)
1352     mode = pifrec['ip_configuration_mode']
1353     if bridge:
1354         log("Configuring %s using %s configuration" % (bridge, mode))
1355         br = open_network_ifcfg(pif)
1356         configure_network(pif, br)
1357         br.close()
1358         f.attach_child(br)
1359     else:
1360         log("Configuring %s using %s configuration" % (interface, mode))
1361         configure_network(pif, f)
1362     f.close()
1363     try:
1364         f.apply()
1365         f.commit()
1366     except Error, e:
1367         log("failed to apply changes: %s" % e.msg)
1368         f.revert()
1369         raise
1370
1371     # We have no code of our own to run here.
1372     pass
1373
1374 def action_force_rewrite(interface, config):
1375     raise Error("Force rewrite is not implemented yet.")
1376
1377 def main(argv=None):
1378     global output_directory, management_pif
1379     
1380     session = None
1381     pif_uuid = None
1382     pif = None
1383
1384     force_interface = None
1385     force_management = False
1386     
1387     if argv is None:
1388         argv = sys.argv
1389
1390     try:
1391         try:
1392             shortops = "h"
1393             longops = [ "output-directory=",
1394                         "pif=", "pif-uuid=",
1395                         "session=",
1396                         "force=",
1397                         "force-interface=",
1398                         "management",
1399                         "device=", "mode=", "ip=", "netmask=", "gateway=",
1400                         "help" ]
1401             arglist, args = getopt.gnu_getopt(argv[1:], shortops, longops)
1402         except getopt.GetoptError, msg:
1403             raise Usage(msg)
1404
1405         force_rewrite_config = {}
1406         
1407         for o,a in arglist:
1408             if o == "--output-directory":
1409                 output_directory = a
1410             elif o == "--pif":
1411                 pif = a
1412             elif o == "--pif-uuid":
1413                 pif_uuid = a
1414             elif o == "--session":
1415                 session = a
1416             elif o == "--force-interface" or o == "--force":
1417                 force_interface = a
1418             elif o == "--management":
1419                 force_management = True
1420             elif o in ["--device", "--mode", "--ip", "--netmask", "--gateway"]:
1421                 force_rewrite_config[o[2:]] = a
1422             elif o == "-h" or o == "--help":
1423                 print __doc__ % {'command-name': os.path.basename(argv[0])}
1424                 return 0
1425
1426         if not debug_mode():
1427             syslog.openlog(os.path.basename(argv[0]))
1428             log("Called as " + str.join(" ", argv))
1429         if len(args) < 1:
1430             raise Usage("Required option <action> not present")
1431         if len(args) > 1:
1432             raise Usage("Too many arguments")
1433
1434         action = args[0]
1435
1436         if not action in ["up", "down", "rewrite", "rewrite-configuration"]:
1437             raise Usage("Unknown action \"%s\"" % action)
1438
1439         # backwards compatibility
1440         if action == "rewrite-configuration": action = "rewrite"
1441         
1442         if output_directory and ( session or pif ):
1443             raise Usage("--session/--pif cannot be used with --output-directory")
1444         if ( session or pif ) and pif_uuid:
1445             raise Usage("--session/--pif and --pif-uuid are mutually exclusive.")
1446         if ( session and not pif ) or ( not session and pif ):
1447             raise Usage("--session and --pif must be used together.")
1448         if force_interface and ( session or pif or pif_uuid ):
1449             raise Usage("--force is mutually exclusive with --session, --pif and --pif-uuid")
1450         if force_interface == "all" and action != "down":
1451             raise Usage("\"--force all\" only valid for down action")
1452         if len(force_rewrite_config) and not (force_interface and action == "rewrite"):
1453             raise Usage("\"--force rewrite\" needed for --device, --mode, --ip, --netmask, and --gateway")
1454
1455         global db
1456         if force_interface:
1457             log("Force interface %s %s" % (force_interface, action))
1458
1459             if action == "rewrite":
1460                 action_force_rewrite(force_interface, force_rewrite_config)
1461             elif action in ["up", "down"]:
1462                 if action == "down" and force_interface == "all":
1463                     raise Error("Force all interfaces down not implemented yet")
1464
1465                 db = DatabaseCache(cache_file=dbcache_file)
1466                 pif = db.get_pif_by_bridge(force_interface)
1467                 management_pif = db.get_management_pif()
1468
1469                 if action == "up":
1470                     action_up(pif)
1471                 elif action == "down":
1472                     action_down(pif)
1473             else:
1474                 raise Error("Unknown action %s"  % action)
1475         else:
1476             db = DatabaseCache(session_ref=session)
1477
1478             if pif_uuid:
1479                 pif = db.get_pif_by_uuid(pif_uuid)
1480
1481             if action == "rewrite" and not pif:
1482                 pass
1483             else:
1484                 if not pif:
1485                     raise Usage("No PIF given")
1486
1487                 if force_management:
1488                     # pif is going to be the management pif 
1489                     management_pif = pif
1490                 else:
1491                     # pif is not going to be the management pif.
1492                     # Search DB cache for pif on same host with management=true
1493                     pifrec = db.get_pif_record(pif)
1494                     management_pif = db.get_management_pif()
1495
1496                 log_pif_action(action, pif)
1497
1498                 if not check_allowed(pif):
1499                     return 0
1500
1501                 if action == "up":
1502                     action_up(pif)
1503                 elif action == "down":
1504                     action_down(pif)
1505                 elif action == "rewrite":
1506                     action_rewrite(pif)
1507                 else:
1508                     raise Error("Unknown action %s"  % action)
1509
1510             # Save cache.
1511             db.save(dbcache_file)
1512         
1513     except Usage, err:
1514         print >>sys.stderr, err.msg
1515         print >>sys.stderr, "For help use --help."
1516         return 2
1517     except Error, err:
1518         log(err.msg)
1519         return 1
1520     
1521     return 0
1522 \f
1523 # The following code allows interface-reconfigure to keep Centos
1524 # network configuration files up-to-date, even though the vswitch
1525 # never uses them.  In turn, that means that "rpm -e vswitch" does not
1526 # have to update any configuration files.
1527
1528 def configure_ethtool(oc, f):
1529     # Options for "ethtool -s"
1530     settings = None
1531     setting_opts = ["autoneg", "speed", "duplex"]
1532     # Options for "ethtool -K"
1533     offload = None
1534     offload_opts = ["rx", "tx", "sg", "tso", "ufo", "gso"]
1535     
1536     for opt in [opt for opt in setting_opts + offload_opts if oc.has_key("ethtool-" + opt)]:
1537         val = oc["ethtool-" + opt]
1538
1539         if opt in ["speed"]:
1540             if val in ["10", "100", "1000"]:
1541                 val = "speed " + val
1542             else:
1543                 log("Invalid value for ethtool-speed = %s. Must be 10|100|1000." % val)
1544                 val = None
1545         elif opt in ["duplex"]:
1546             if val in ["half", "full"]:
1547                 val = "duplex " + val
1548             else:
1549                 log("Invalid value for ethtool-duplex = %s. Must be half|full." % val)
1550                 val = None
1551         elif opt in ["autoneg"] + offload_opts:
1552             if val in ["true", "on"]:
1553                 val = opt + " on"
1554             elif val in ["false", "off"]:
1555                 val = opt + " off"
1556             else:
1557                 log("Invalid value for ethtool-%s = %s. Must be on|true|off|false." % (opt, val))
1558                 val = None
1559
1560         if opt in setting_opts:
1561             if val and settings:
1562                 settings = settings + " " + val
1563             else:
1564                 settings = val
1565         elif opt in offload_opts:
1566             if val and offload:
1567                 offload = offload + " " + val
1568             else:
1569                 offload = val
1570
1571     if settings:
1572         f.write("ETHTOOL_OPTS=\"%s\"\n" % settings)
1573     if offload:
1574         f.write("ETHTOOL_OFFLOAD_OPTS=\"%s\"\n" % offload)
1575
1576 def configure_mtu(oc, f):
1577     if not oc.has_key('mtu'):
1578         return
1579     
1580     try:
1581         mtu = int(oc['mtu'])
1582         f.write("MTU=%d\n" % mtu)
1583     except ValueError, x:
1584         log("Invalid value for mtu = %s" % mtu)
1585
1586 def configure_static_routes(interface, oc, f):
1587     """Open a route-<interface> file for static routes.
1588     
1589     Opens the static routes configuration file for interface and writes one
1590     line for each route specified in the network's other config "static-routes" value.
1591     E.g. if
1592            interface ( RO): xenbr1
1593            other-config (MRW): static-routes: 172.16.0.0/15/192.168.0.3,172.18.0.0/16/192.168.0.4;...
1594
1595     Then route-xenbr1 should be
1596           172.16.0.0/15 via 192.168.0.3 dev xenbr1
1597           172.18.0.0/16 via 192.168.0.4 dev xenbr1
1598     """
1599     fname = "route-%s" % interface
1600     if oc.has_key('static-routes'):
1601         # The key is present - extract comma seperates entries
1602         lines = oc['static-routes'].split(',')
1603     else:
1604         # The key is not present, i.e. there are no static routes
1605         lines = []
1606
1607     child = ConfigurationFile(fname)
1608     child.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \
1609             (os.path.basename(child.path()), os.path.basename(sys.argv[0])))
1610
1611     try:
1612         for l in lines:
1613             network, masklen, gateway = l.split('/')
1614             child.write("%s/%s via %s dev %s\n" % (network, masklen, gateway, interface))
1615             
1616         f.attach_child(child)
1617         child.close()
1618
1619     except ValueError, e:
1620         log("Error in other-config['static-routes'] format for network %s: %s" % (interface, e))
1621
1622 def __open_ifcfg(interface):
1623     """Open a network interface configuration file.
1624
1625     Opens the configuration file for interface, writes a header and
1626     common options and returns the file object.
1627     """
1628     fname = "ifcfg-%s" % interface
1629     f = ConfigurationFile(fname)
1630     
1631     f.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \
1632             (os.path.basename(f.path()), os.path.basename(sys.argv[0])))
1633     f.write("XEMANAGED=yes\n")
1634     f.write("DEVICE=%s\n" % interface)
1635     f.write("ONBOOT=no\n")
1636     
1637     return f
1638
1639 def open_network_ifcfg(pif):    
1640     bridge = bridge_name(pif)
1641     interface = interface_name(pif)
1642     if bridge:
1643         return __open_ifcfg(bridge)
1644     else:
1645         return __open_ifcfg(interface)
1646
1647
1648 def open_pif_ifcfg(pif):
1649     pifrec = db.get_pif_record(pif)
1650     
1651     log("Configuring %s (%s)" % (interface_name(pif), pifrec['MAC']))
1652     
1653     f = __open_ifcfg(interface_name(pif))
1654
1655     if pifrec.has_key('other_config'):
1656         configure_ethtool(pifrec['other_config'], f)
1657         configure_mtu(pifrec['other_config'], f)
1658
1659     return f
1660
1661 def configure_network(pif, f):
1662     """Write the configuration file for a network.
1663
1664     Writes configuration derived from the network object into the relevant 
1665     ifcfg file.  The configuration file is passed in, but if the network is 
1666     bridgeless it will be ifcfg-<interface>, otherwise it will be ifcfg-<bridge>.
1667
1668     This routine may also write ifcfg files of the networks corresponding to other PIFs
1669     in order to maintain consistency.
1670
1671     params:
1672         pif:  Opaque_ref of pif
1673         f :   ConfigurationFile(/path/to/ifcfg) to which we append network configuration
1674     """
1675     
1676     pifrec = db.get_pif_record(pif)
1677     nw = pifrec['network']
1678     nwrec = db.get_network_record(nw)
1679     oc = None
1680     bridge = bridge_name(pif)
1681     interface = interface_name(pif)
1682     if bridge:
1683         device = bridge
1684     else:
1685         device = interface
1686
1687     if nwrec.has_key('other_config'):
1688         configure_ethtool(nwrec['other_config'], f)
1689         configure_mtu(nwrec['other_config'], f)
1690         configure_static_routes(device, nwrec['other_config'], f)
1691
1692     
1693     if pifrec.has_key('other_config'):
1694         oc = pifrec['other_config']
1695
1696     if device == bridge:
1697         f.write("TYPE=Bridge\n")
1698         f.write("DELAY=0\n")
1699         f.write("STP=off\n")
1700         f.write("PIFDEV=%s\n" % interface_name(pif))
1701
1702     if pifrec['ip_configuration_mode'] == "DHCP":
1703         f.write("BOOTPROTO=dhcp\n")
1704         f.write("PERSISTENT_DHCLIENT=yes\n")
1705     elif pifrec['ip_configuration_mode'] == "Static":
1706         f.write("BOOTPROTO=none\n") 
1707         f.write("NETMASK=%(netmask)s\n" % pifrec)
1708         f.write("IPADDR=%(IP)s\n" % pifrec)
1709         f.write("GATEWAY=%(gateway)s\n" % pifrec)
1710     elif pifrec['ip_configuration_mode'] == "None":
1711         f.write("BOOTPROTO=none\n")
1712     else:
1713         raise Error("Unknown ip-configuration-mode %s" % pifrec['ip_configuration_mode'])
1714
1715     if pifrec.has_key('DNS') and pifrec['DNS'] != "":
1716         ServerList = pifrec['DNS'].split(",")
1717         for i in range(len(ServerList)): f.write("DNS%d=%s\n" % (i+1, ServerList[i]))
1718     if oc and oc.has_key('domain'):
1719         f.write("DOMAIN='%s'\n" % oc['domain'].replace(',', ' '))
1720
1721     # We only allow one ifcfg-xenbr* to have PEERDNS=yes and there can be only one GATEWAYDEV in /etc/sysconfig/network.
1722     # The peerdns pif will be the one with pif::other-config:peerdns=true, or the mgmt pif if none have this set.
1723     # The gateway pif will be the one with pif::other-config:defaultroute=true, or the mgmt pif if none have this set.
1724
1725     # Work out which pif on this host should be the one with PEERDNS=yes and which should be the GATEWAYDEV
1726     #
1727     # Note: we prune out the bond master pif (if it exists).
1728     # This is because when we are called to bring up an interface with a bond master, it is implicit that
1729     # we should bring down that master.
1730     pifs_on_host = [ __pif for __pif in db.get_all_pifs() if
1731                      not __pif in get_bond_masters_of_pif(pif) ]
1732     other_pifs_on_host = [ __pif for __pif in pifs_on_host if __pif != pif ]
1733
1734     peerdns_pif = None
1735     defaultroute_pif = None
1736     
1737     # loop through all the pifs on this host looking for one with
1738     #   other-config:peerdns = true, and one with
1739     #   other-config:default-route=true
1740     for __pif in pifs_on_host:
1741         __pifrec = db.get_pif_record(__pif)
1742         __oc = __pifrec['other_config']
1743         if __oc.has_key('peerdns') and __oc['peerdns'] == 'true':
1744             if peerdns_pif == None:
1745                 peerdns_pif = __pif
1746             else:
1747                 log('Warning: multiple pifs with "peerdns=true" - choosing %s and ignoring %s' % \
1748                         (db.get_pif_record(peerdns_pif)['device'], __pifrec['device']))
1749         if __oc.has_key('defaultroute') and __oc['defaultroute'] == 'true':
1750             if defaultroute_pif == None:
1751                 defaultroute_pif = __pif
1752             else:
1753                 log('Warning: multiple pifs with "defaultroute=true" - choosing %s and ignoring %s' % \
1754                         (db.get_pif_record(defaultroute_pif)['device'], __pifrec['device']))
1755     
1756     # If no pif is explicitly specified then use the mgmt pif for peerdns/defaultroute
1757     if peerdns_pif == None:
1758         peerdns_pif = management_pif
1759     if defaultroute_pif == None:
1760         defaultroute_pif = management_pif
1761         
1762     # Update all the other network's ifcfg files and ensure consistency
1763     for __pif in other_pifs_on_host:
1764         __f = open_network_ifcfg(__pif)
1765         peerdns_line_wanted = 'PEERDNS=%s\n' % ((__pif == peerdns_pif) and 'yes' or 'no')
1766         lines =  __f.readlines()
1767
1768         if not peerdns_line_wanted in lines:
1769             # the PIF selected for DNS has changed and as a result this ifcfg file needs rewriting
1770             for line in lines:
1771                 if not line.lstrip().startswith('PEERDNS'):
1772                     __f.write(line)
1773             log("Setting %s in %s" % (peerdns_line_wanted.strip(), __f.path()))
1774             __f.write(peerdns_line_wanted)
1775             __f.close()
1776             f.attach_child(__f)
1777
1778         else:
1779             # There is no need to change this ifcfg file.  So don't attach_child.
1780             pass
1781         
1782     # ... and for this pif too
1783     f.write('PEERDNS=%s\n' % ((pif == peerdns_pif) and 'yes' or 'no'))
1784     
1785     # Update gatewaydev
1786     fnetwork = ConfigurationFile("network", "/etc/sysconfig")
1787     for line in fnetwork.readlines():
1788         if line.lstrip().startswith('GATEWAY') :
1789             continue
1790         fnetwork.write(line)
1791     if defaultroute_pif:
1792         gatewaydev = bridge_name(defaultroute_pif)
1793         if not gatewaydev:
1794             gatewaydev = interface_name(defaultroute_pif)
1795         fnetwork.write('GATEWAYDEV=%s\n' % gatewaydev)
1796     fnetwork.close()
1797     f.attach_child(fnetwork)
1798
1799     return
1800
1801
1802 def configure_physical_interface(pif):
1803     """Write the configuration for a physical interface.
1804
1805     Writes the configuration file for the physical interface described by
1806     the pif object.
1807
1808     Returns the open file handle for the interface configuration file.
1809     """
1810
1811     pifrec = db.get_pif_record(pif)
1812
1813     f = open_pif_ifcfg(pif)
1814     
1815     f.write("TYPE=Ethernet\n")
1816     f.write("HWADDR=%(MAC)s\n" % pifrec)
1817
1818     return f
1819
1820 def configure_bond_interface(pif):
1821     """Write the configuration for a bond interface.
1822
1823     Writes the configuration file for the bond interface described by
1824     the pif object. Handles writing the configuration for the slave
1825     interfaces.
1826
1827     Returns the open file handle for the bond interface configuration
1828     file.
1829     """
1830
1831     pifrec = db.get_pif_record(pif)
1832     oc = pifrec['other_config']
1833     f = open_pif_ifcfg(pif)
1834
1835     if pifrec['MAC'] != "":
1836         f.write("MACADDR=%s\n" % pifrec['MAC'])
1837
1838     for slave in get_bond_slaves_of_pif(pif):
1839         s = configure_physical_interface(slave)
1840         s.write("MASTER=%(device)s\n" % pifrec)
1841         s.write("SLAVE=yes\n")
1842         s.close()
1843         f.attach_child(s)
1844
1845     # The bond option defaults
1846     bond_options = { 
1847         "mode":   "balance-slb",
1848         "miimon": "100",
1849         "downdelay": "200",
1850         "updelay": "31000",
1851         "use_carrier": "1",
1852         }
1853
1854     # override defaults with values from other-config whose keys being with "bond-"
1855     overrides = filter(lambda (key,val): key.startswith("bond-"), oc.items())
1856     overrides = map(lambda (key,val): (key[5:], val), overrides)
1857     bond_options.update(overrides)
1858
1859     # write the bond options to ifcfg-bondX
1860     f.write('BONDING_OPTS="')
1861     for (name,val) in bond_options.items():
1862         f.write("%s=%s " % (name,val))
1863     f.write('"\n')
1864     return f
1865
1866 def configure_vlan_interface(pif):
1867     """Write the configuration for a VLAN interface.
1868
1869     Writes the configuration file for the VLAN interface described by
1870     the pif object. Handles writing the configuration for the master
1871     interface if necessary.
1872
1873     Returns the open file handle for the VLAN interface configuration
1874     file.
1875     """
1876
1877     slave = configure_pif(get_vlan_slave_of_pif(pif))
1878     slave.close()
1879
1880     f = open_pif_ifcfg(pif)
1881     f.write("VLAN=yes\n")
1882     f.attach_child(slave)
1883     
1884     return f
1885
1886 def configure_pif(pif):
1887     """Write the configuration for a PIF object.
1888
1889     Writes the configuration file the PIF and all dependent
1890     interfaces (bond slaves and VLAN masters etc).
1891
1892     Returns the open file handle for the interface configuration file.
1893     """
1894
1895     pifrec = db.get_pif_record(pif)
1896
1897     if pifrec['VLAN'] != '-1':
1898         f = configure_vlan_interface(pif)
1899     elif len(pifrec['bond_master_of']) != 0:
1900         f = configure_bond_interface(pif)
1901     else:
1902         f = configure_physical_interface(pif)
1903
1904     bridge = bridge_name(pif)
1905     if bridge:
1906         f.write("BRIDGE=%s\n" % bridge)
1907
1908     return f
1909
1910 def unconfigure_pif(pif):
1911     """Clear up the files created by configure_pif"""
1912     f = open_pif_ifcfg(pif)
1913     log("Unlinking stale file %s" % f.path())
1914     f.unlink()
1915     return f
1916 \f
1917 if __name__ == "__main__":
1918     rc = 1
1919     try:
1920         rc = main()
1921     except:
1922         ex = sys.exc_info()
1923         err = traceback.format_exception(*ex)
1924         for exline in err:
1925             log(exline)
1926
1927     if not debug_mode():
1928         syslog.closelog()
1929         
1930     sys.exit(rc)