xenserver: Drop "init-dbcache" by making PIF optional for "rewrite".
[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     if vlan_slave:
1241         up_netdev(ipdev_name(vlan_slave))
1242     for physdev_pif in set(physdev_pifs) - set(bond_slave_physdev_pifs):
1243         configure_physdev(physdev_pif)
1244
1245     # Configure network device for local port.
1246     configure_local_port(pif)
1247
1248     # Update /etc/issue (which contains the IP address of the management interface)
1249     os.system("/sbin/update-issue")
1250
1251     if bond_slaves:
1252         # There seems to be a race somewhere: without this sleep, using
1253         # XenCenter to create a bond that becomes the management interface
1254         # fails with "The underlying connection was closed: A connection that
1255         # was expected to be kept alive was closed by the server." on every
1256         # second or third try, even though /var/log/messages doesn't show
1257         # anything unusual.
1258         #
1259         # The race is probably present even without vswitch, but bringing up a
1260         # bond without vswitch involves a built-in pause of 10 seconds or more
1261         # to wait for the bond to transition from learning to forwarding state.
1262         time.sleep(5)
1263         
1264 def action_down(pif):
1265     rec = db.get_pif_record(pif)    
1266     interface = interface_name(pif)
1267     bridge = bridge_name(pif)
1268     ipdev = ipdev_name(pif)
1269
1270     # Support "rpm -e vswitch" gracefully by keeping Centos configuration
1271     # files up-to-date, even though we don't use them or need them.
1272     f = unconfigure_pif(pif)
1273     if bridge:
1274         br = open_network_ifcfg(pif)
1275         log("Unlinking stale file %s" % br.path())
1276         br.unlink()
1277         f.attach_child(br)
1278     try:
1279         f.apply()
1280         f.commit()
1281     except Error, e:
1282         log("action_down failed to apply changes: %s" % e.msg)
1283         f.revert()
1284         raise
1285
1286     argv = []
1287     if rec['VLAN'] != '-1':
1288         # Get rid of the VLAN device itself.
1289         down_netdev(ipdev)
1290         argv += interface_deconfigure_commands(ipdev)
1291
1292         # If the VLAN's slave is attached, stop here.
1293         slave = get_vlan_slave_of_pif(pif)
1294         if db.get_pif_record(slave)['currently_attached']:
1295             log("VLAN slave is currently attached")
1296             modify_config(argv)
1297             return
1298         
1299         # If the VLAN's slave has other VLANs that are attached, stop here.
1300         masters = get_vlan_masters_of_pif(slave)
1301         for m in masters:
1302             if m != pif and db.get_pif_record(m)['currently_attached']:
1303                 log("VLAN slave has other master %s" % interface_naem(m))
1304                 modify_config(argv)
1305                 return
1306
1307         # Otherwise, take down the VLAN's slave too.
1308         log("No more masters, bring down vlan slave %s" % interface_name(slave))
1309         pif = slave
1310     else:
1311         # Stop here if this PIF has attached VLAN masters.
1312         vlan_masters = get_vlan_masters_of_pif(pif)
1313         log("VLAN masters of %s - %s" % (rec['device'], [interface_name(m) for m in vlan_masters]))
1314         for m in vlan_masters:
1315             if db.get_pif_record(m)['currently_attached']:
1316                 log("Leaving %s up due to currently attached VLAN master %s" % (interface, interface_name(m)))
1317                 return
1318
1319     # pif is now either a bond or a physical device which needs to be
1320     # brought down.  pif might have changed so re-check all its attributes.
1321     rec = db.get_pif_record(pif)
1322     interface = interface_name(pif)
1323     bridge = bridge_name(pif)
1324     ipdev = ipdev_name(pif)
1325
1326
1327     bond_slaves = get_bond_slaves_of_pif(pif)
1328     log("bond slaves of %s - %s" % (rec['device'], [interface_name(s) for s in bond_slaves]))
1329     for slave in bond_slaves:
1330         slave_interface = interface_name(slave)
1331         log("bring down bond slave %s" % slave_interface)
1332         argv += interface_deconfigure_commands(slave_interface)
1333         down_netdev(slave_interface)
1334
1335     argv += interface_deconfigure_commands(ipdev)
1336     down_netdev(ipdev)
1337
1338     argv += ['--del-match', 'bridge.%s.*' % datapath_name(pif)]
1339     argv += ['--del-match', 'bonding.%s.[!0-9]*' % interface]
1340     modify_config(argv)
1341
1342 def action_rewrite(pif):
1343     # Support "rpm -e vswitch" gracefully by keeping Centos configuration
1344     # files up-to-date, even though we don't use them or need them.
1345     pifrec = db.get_pif_record(pif)
1346     f = configure_pif(pif)
1347     interface = interface_name(pif)
1348     bridge = bridge_name(pif)
1349     mode = pifrec['ip_configuration_mode']
1350     if bridge:
1351         log("Configuring %s using %s configuration" % (bridge, mode))
1352         br = open_network_ifcfg(pif)
1353         configure_network(pif, br)
1354         br.close()
1355         f.attach_child(br)
1356     else:
1357         log("Configuring %s using %s configuration" % (interface, mode))
1358         configure_network(pif, f)
1359     f.close()
1360     try:
1361         f.apply()
1362         f.commit()
1363     except Error, e:
1364         log("failed to apply changes: %s" % e.msg)
1365         f.revert()
1366         raise
1367
1368     # We have no code of our own to run here.
1369     pass
1370
1371 def main(argv=None):
1372     global output_directory, management_pif
1373     
1374     session = None
1375     pif_uuid = None
1376     pif = None
1377
1378     force_interface = None
1379     force_management = False
1380     
1381     if argv is None:
1382         argv = sys.argv
1383
1384     try:
1385         try:
1386             shortops = "h"
1387             longops = [ "output-directory=",
1388                         "pif=", "pif-uuid=",
1389                         "session=",
1390                         "force=",
1391                         "force-interface=",
1392                         "management",
1393                         "test-mode",
1394                         "device=", "mode=", "ip=", "netmask=", "gateway=",
1395                         "help" ]
1396             arglist, args = getopt.gnu_getopt(argv[1:], shortops, longops)
1397         except getopt.GetoptError, msg:
1398             raise Usage(msg)
1399
1400         force_rewrite_config = {}
1401         
1402         for o,a in arglist:
1403             if o == "--output-directory":
1404                 output_directory = a
1405             elif o == "--pif":
1406                 pif = a
1407             elif o == "--pif-uuid":
1408                 pif_uuid = a
1409             elif o == "--session":
1410                 session = a
1411             elif o == "--force-interface" or o == "--force":
1412                 force_interface = a
1413             elif o == "--management":
1414                 force_management = True
1415             elif o in ["--device", "--mode", "--ip", "--netmask", "--gateway"]:
1416                 force_rewrite_config[o[2:]] = a
1417             elif o == "-h" or o == "--help":
1418                 print __doc__ % {'command-name': os.path.basename(argv[0])}
1419                 return 0
1420
1421         if not debug_mode():
1422             syslog.openlog(os.path.basename(argv[0]))
1423             log("Called as " + str.join(" ", argv))
1424         if len(args) < 1:
1425             raise Usage("Required option <action> not present")
1426         if len(args) > 1:
1427             raise Usage("Too many arguments")
1428
1429         action = args[0]
1430         # backwards compatibility
1431         if action == "rewrite-configuration": action = "rewrite"
1432         
1433         if output_directory and ( session or pif ):
1434             raise Usage("--session/--pif cannot be used with --output-directory")
1435         if ( session or pif ) and pif_uuid:
1436             raise Usage("--session/--pif and --pif-uuid are mutually exclusive.")
1437         if ( session and not pif ) or ( not session and pif ):
1438             raise Usage("--session and --pif must be used together.")
1439         if force_interface and ( session or pif or pif_uuid ):
1440             raise Usage("--force is mutually exclusive with --session, --pif and --pif-uuid")
1441         if len(force_rewrite_config) and not (force_interface and action == "rewrite"):
1442             raise Usage("\"--force rewrite\" needed for --device, --mode, --ip, --netmask, and --gateway")
1443
1444         global db
1445         if force_interface:
1446             log("Force interface %s %s" % (force_interface, action))
1447
1448             if action == "rewrite":
1449                 action_force_rewrite(force_interface, force_rewrite_config)
1450             else:
1451                 db = DatabaseCache(cache_file=dbcache_file)
1452                 pif = db.get_pif_by_bridge(force_interface)
1453                 management_pif = db.get_management_pif()
1454
1455                 if action == "up":
1456                     action_up(pif)
1457                 elif action == "down":
1458                     action_down(pif)
1459                 else:
1460                     raise Usage("Unknown action %s"  % action)
1461         else:
1462             db = DatabaseCache(session_ref=session)
1463
1464             if pif_uuid:
1465                 pif = db.get_pif_by_uuid(pif_uuid)
1466
1467             if action == "rewrite" and not pif:
1468                 pass
1469             else:
1470                 if not pif:
1471                     raise Usage("No PIF given")
1472
1473                 if force_management:
1474                     # pif is going to be the management pif 
1475                     management_pif = pif
1476                 else:
1477                     # pif is not going to be the management pif.
1478                     # Search DB cache for pif on same host with management=true
1479                     pifrec = db.get_pif_record(pif)
1480                     management_pif = db.get_management_pif()
1481
1482                 log_pif_action(action, pif)
1483
1484                 if not check_allowed(pif):
1485                     return 0
1486
1487                 if action == "up":
1488                     action_up(pif)
1489                 elif action == "down":
1490                     action_down(pif)
1491                 elif action == "rewrite":
1492                     action_rewrite(pif)
1493                 else:
1494                     raise Usage("Unknown action %s"  % action)
1495
1496             # Save cache.
1497             db.save(dbcache_file)
1498         
1499     except Usage, err:
1500         print >>sys.stderr, err.msg
1501         print >>sys.stderr, "For help use --help."
1502         return 2
1503     except Error, err:
1504         log(err.msg)
1505         return 1
1506     
1507     return 0
1508 \f
1509 # The following code allows interface-reconfigure to keep Centos
1510 # network configuration files up-to-date, even though the vswitch
1511 # never uses them.  In turn, that means that "rpm -e vswitch" does not
1512 # have to update any configuration files.
1513
1514 def configure_ethtool(oc, f):
1515     # Options for "ethtool -s"
1516     settings = None
1517     setting_opts = ["autoneg", "speed", "duplex"]
1518     # Options for "ethtool -K"
1519     offload = None
1520     offload_opts = ["rx", "tx", "sg", "tso", "ufo", "gso"]
1521     
1522     for opt in [opt for opt in setting_opts + offload_opts if oc.has_key("ethtool-" + opt)]:
1523         val = oc["ethtool-" + opt]
1524
1525         if opt in ["speed"]:
1526             if val in ["10", "100", "1000"]:
1527                 val = "speed " + val
1528             else:
1529                 log("Invalid value for ethtool-speed = %s. Must be 10|100|1000." % val)
1530                 val = None
1531         elif opt in ["duplex"]:
1532             if val in ["half", "full"]:
1533                 val = "duplex " + val
1534             else:
1535                 log("Invalid value for ethtool-duplex = %s. Must be half|full." % val)
1536                 val = None
1537         elif opt in ["autoneg"] + offload_opts:
1538             if val in ["true", "on"]:
1539                 val = opt + " on"
1540             elif val in ["false", "off"]:
1541                 val = opt + " off"
1542             else:
1543                 log("Invalid value for ethtool-%s = %s. Must be on|true|off|false." % (opt, val))
1544                 val = None
1545
1546         if opt in setting_opts:
1547             if val and settings:
1548                 settings = settings + " " + val
1549             else:
1550                 settings = val
1551         elif opt in offload_opts:
1552             if val and offload:
1553                 offload = offload + " " + val
1554             else:
1555                 offload = val
1556
1557     if settings:
1558         f.write("ETHTOOL_OPTS=\"%s\"\n" % settings)
1559     if offload:
1560         f.write("ETHTOOL_OFFLOAD_OPTS=\"%s\"\n" % offload)
1561
1562 def configure_mtu(oc, f):
1563     if not oc.has_key('mtu'):
1564         return
1565     
1566     try:
1567         mtu = int(oc['mtu'])
1568         f.write("MTU=%d\n" % mtu)
1569     except ValueError, x:
1570         log("Invalid value for mtu = %s" % mtu)
1571
1572 def configure_static_routes(interface, oc, f):
1573     """Open a route-<interface> file for static routes.
1574     
1575     Opens the static routes configuration file for interface and writes one
1576     line for each route specified in the network's other config "static-routes" value.
1577     E.g. if
1578            interface ( RO): xenbr1
1579            other-config (MRW): static-routes: 172.16.0.0/15/192.168.0.3,172.18.0.0/16/192.168.0.4;...
1580
1581     Then route-xenbr1 should be
1582           172.16.0.0/15 via 192.168.0.3 dev xenbr1
1583           172.18.0.0/16 via 192.168.0.4 dev xenbr1
1584     """
1585     fname = "route-%s" % interface
1586     if oc.has_key('static-routes'):
1587         # The key is present - extract comma seperates entries
1588         lines = oc['static-routes'].split(',')
1589     else:
1590         # The key is not present, i.e. there are no static routes
1591         lines = []
1592
1593     child = ConfigurationFile(fname)
1594     child.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \
1595             (os.path.basename(child.path()), os.path.basename(sys.argv[0])))
1596
1597     try:
1598         for l in lines:
1599             network, masklen, gateway = l.split('/')
1600             child.write("%s/%s via %s dev %s\n" % (network, masklen, gateway, interface))
1601             
1602         f.attach_child(child)
1603         child.close()
1604
1605     except ValueError, e:
1606         log("Error in other-config['static-routes'] format for network %s: %s" % (interface, e))
1607
1608 def __open_ifcfg(interface):
1609     """Open a network interface configuration file.
1610
1611     Opens the configuration file for interface, writes a header and
1612     common options and returns the file object.
1613     """
1614     fname = "ifcfg-%s" % interface
1615     f = ConfigurationFile(fname)
1616     
1617     f.write("# DO NOT EDIT: This file (%s) was autogenerated by %s\n" % \
1618             (os.path.basename(f.path()), os.path.basename(sys.argv[0])))
1619     f.write("XEMANAGED=yes\n")
1620     f.write("DEVICE=%s\n" % interface)
1621     f.write("ONBOOT=no\n")
1622     
1623     return f
1624
1625 def open_network_ifcfg(pif):    
1626     bridge = bridge_name(pif)
1627     interface = interface_name(pif)
1628     if bridge:
1629         return __open_ifcfg(bridge)
1630     else:
1631         return __open_ifcfg(interface)
1632
1633
1634 def open_pif_ifcfg(pif):
1635     pifrec = db.get_pif_record(pif)
1636     
1637     log("Configuring %s (%s)" % (interface_name(pif), pifrec['MAC']))
1638     
1639     f = __open_ifcfg(interface_name(pif))
1640
1641     if pifrec.has_key('other_config'):
1642         configure_ethtool(pifrec['other_config'], f)
1643         configure_mtu(pifrec['other_config'], f)
1644
1645     return f
1646
1647 def configure_network(pif, f):
1648     """Write the configuration file for a network.
1649
1650     Writes configuration derived from the network object into the relevant 
1651     ifcfg file.  The configuration file is passed in, but if the network is 
1652     bridgeless it will be ifcfg-<interface>, otherwise it will be ifcfg-<bridge>.
1653
1654     This routine may also write ifcfg files of the networks corresponding to other PIFs
1655     in order to maintain consistency.
1656
1657     params:
1658         pif:  Opaque_ref of pif
1659         f :   ConfigurationFile(/path/to/ifcfg) to which we append network configuration
1660     """
1661     
1662     pifrec = db.get_pif_record(pif)
1663     nw = pifrec['network']
1664     nwrec = db.get_network_record(nw)
1665     oc = None
1666     bridge = bridge_name(pif)
1667     interface = interface_name(pif)
1668     if bridge:
1669         device = bridge
1670     else:
1671         device = interface
1672
1673     if nwrec.has_key('other_config'):
1674         configure_ethtool(nwrec['other_config'], f)
1675         configure_mtu(nwrec['other_config'], f)
1676         configure_static_routes(device, nwrec['other_config'], f)
1677
1678     
1679     if pifrec.has_key('other_config'):
1680         oc = pifrec['other_config']
1681
1682     if device == bridge:
1683         f.write("TYPE=Bridge\n")
1684         f.write("DELAY=0\n")
1685         f.write("STP=off\n")
1686         f.write("PIFDEV=%s\n" % interface_name(pif))
1687
1688     if pifrec['ip_configuration_mode'] == "DHCP":
1689         f.write("BOOTPROTO=dhcp\n")
1690         f.write("PERSISTENT_DHCLIENT=yes\n")
1691     elif pifrec['ip_configuration_mode'] == "Static":
1692         f.write("BOOTPROTO=none\n") 
1693         f.write("NETMASK=%(netmask)s\n" % pifrec)
1694         f.write("IPADDR=%(IP)s\n" % pifrec)
1695         f.write("GATEWAY=%(gateway)s\n" % pifrec)
1696     elif pifrec['ip_configuration_mode'] == "None":
1697         f.write("BOOTPROTO=none\n")
1698     else:
1699         raise Error("Unknown ip-configuration-mode %s" % pifrec['ip_configuration_mode'])
1700
1701     if pifrec.has_key('DNS') and pifrec['DNS'] != "":
1702         ServerList = pifrec['DNS'].split(",")
1703         for i in range(len(ServerList)): f.write("DNS%d=%s\n" % (i+1, ServerList[i]))
1704     if oc and oc.has_key('domain'):
1705         f.write("DOMAIN='%s'\n" % oc['domain'].replace(',', ' '))
1706
1707     # We only allow one ifcfg-xenbr* to have PEERDNS=yes and there can be only one GATEWAYDEV in /etc/sysconfig/network.
1708     # The peerdns pif will be the one with pif::other-config:peerdns=true, or the mgmt pif if none have this set.
1709     # The gateway pif will be the one with pif::other-config:defaultroute=true, or the mgmt pif if none have this set.
1710
1711     # Work out which pif on this host should be the one with PEERDNS=yes and which should be the GATEWAYDEV
1712     #
1713     # Note: we prune out the bond master pif (if it exists).
1714     # This is because when we are called to bring up an interface with a bond master, it is implicit that
1715     # we should bring down that master.
1716     pifs_on_host = [ __pif for __pif in db.get_all_pifs() if
1717                      not __pif in get_bond_masters_of_pif(pif) ]
1718     other_pifs_on_host = [ __pif for __pif in pifs_on_host if __pif != pif ]
1719
1720     peerdns_pif = None
1721     defaultroute_pif = None
1722     
1723     # loop through all the pifs on this host looking for one with
1724     #   other-config:peerdns = true, and one with
1725     #   other-config:default-route=true
1726     for __pif in pifs_on_host:
1727         __pifrec = db.get_pif_record(__pif)
1728         __oc = __pifrec['other_config']
1729         if __oc.has_key('peerdns') and __oc['peerdns'] == 'true':
1730             if peerdns_pif == None:
1731                 peerdns_pif = __pif
1732             else:
1733                 log('Warning: multiple pifs with "peerdns=true" - choosing %s and ignoring %s' % \
1734                         (db.get_pif_record(peerdns_pif)['device'], __pifrec['device']))
1735         if __oc.has_key('defaultroute') and __oc['defaultroute'] == 'true':
1736             if defaultroute_pif == None:
1737                 defaultroute_pif = __pif
1738             else:
1739                 log('Warning: multiple pifs with "defaultroute=true" - choosing %s and ignoring %s' % \
1740                         (db.get_pif_record(defaultroute_pif)['device'], __pifrec['device']))
1741     
1742     # If no pif is explicitly specified then use the mgmt pif for peerdns/defaultroute
1743     if peerdns_pif == None:
1744         peerdns_pif = management_pif
1745     if defaultroute_pif == None:
1746         defaultroute_pif = management_pif
1747         
1748     # Update all the other network's ifcfg files and ensure consistency
1749     for __pif in other_pifs_on_host:
1750         __f = open_network_ifcfg(__pif)
1751         peerdns_line_wanted = 'PEERDNS=%s\n' % ((__pif == peerdns_pif) and 'yes' or 'no')
1752         lines =  __f.readlines()
1753
1754         if not peerdns_line_wanted in lines:
1755             # the PIF selected for DNS has changed and as a result this ifcfg file needs rewriting
1756             for line in lines:
1757                 if not line.lstrip().startswith('PEERDNS'):
1758                     __f.write(line)
1759             log("Setting %s in %s" % (peerdns_line_wanted.strip(), __f.path()))
1760             __f.write(peerdns_line_wanted)
1761             __f.close()
1762             f.attach_child(__f)
1763
1764         else:
1765             # There is no need to change this ifcfg file.  So don't attach_child.
1766             pass
1767         
1768     # ... and for this pif too
1769     f.write('PEERDNS=%s\n' % ((pif == peerdns_pif) and 'yes' or 'no'))
1770     
1771     # Update gatewaydev
1772     fnetwork = ConfigurationFile("network", "/etc/sysconfig")
1773     for line in fnetwork.readlines():
1774         if line.lstrip().startswith('GATEWAY') :
1775             continue
1776         fnetwork.write(line)
1777     if defaultroute_pif:
1778         gatewaydev = bridge_name(defaultroute_pif)
1779         if not gatewaydev:
1780             gatewaydev = interface_name(defaultroute_pif)
1781         fnetwork.write('GATEWAYDEV=%s\n' % gatewaydev)
1782     fnetwork.close()
1783     f.attach_child(fnetwork)
1784
1785     return
1786
1787
1788 def configure_physical_interface(pif):
1789     """Write the configuration for a physical interface.
1790
1791     Writes the configuration file for the physical interface described by
1792     the pif object.
1793
1794     Returns the open file handle for the interface configuration file.
1795     """
1796
1797     pifrec = db.get_pif_record(pif)
1798
1799     f = open_pif_ifcfg(pif)
1800     
1801     f.write("TYPE=Ethernet\n")
1802     f.write("HWADDR=%(MAC)s\n" % pifrec)
1803
1804     return f
1805
1806 def configure_bond_interface(pif):
1807     """Write the configuration for a bond interface.
1808
1809     Writes the configuration file for the bond interface described by
1810     the pif object. Handles writing the configuration for the slave
1811     interfaces.
1812
1813     Returns the open file handle for the bond interface configuration
1814     file.
1815     """
1816
1817     pifrec = db.get_pif_record(pif)
1818     oc = pifrec['other_config']
1819     f = open_pif_ifcfg(pif)
1820
1821     if pifrec['MAC'] != "":
1822         f.write("MACADDR=%s\n" % pifrec['MAC'])
1823
1824     for slave in get_bond_slaves_of_pif(pif):
1825         s = configure_physical_interface(slave)
1826         s.write("MASTER=%(device)s\n" % pifrec)
1827         s.write("SLAVE=yes\n")
1828         s.close()
1829         f.attach_child(s)
1830
1831     # The bond option defaults
1832     bond_options = { 
1833         "mode":   "balance-slb",
1834         "miimon": "100",
1835         "downdelay": "200",
1836         "updelay": "31000",
1837         "use_carrier": "1",
1838         }
1839
1840     # override defaults with values from other-config whose keys being with "bond-"
1841     overrides = filter(lambda (key,val): key.startswith("bond-"), oc.items())
1842     overrides = map(lambda (key,val): (key[5:], val), overrides)
1843     bond_options.update(overrides)
1844
1845     # write the bond options to ifcfg-bondX
1846     f.write('BONDING_OPTS="')
1847     for (name,val) in bond_options.items():
1848         f.write("%s=%s " % (name,val))
1849     f.write('"\n')
1850     return f
1851
1852 def configure_vlan_interface(pif):
1853     """Write the configuration for a VLAN interface.
1854
1855     Writes the configuration file for the VLAN interface described by
1856     the pif object. Handles writing the configuration for the master
1857     interface if necessary.
1858
1859     Returns the open file handle for the VLAN interface configuration
1860     file.
1861     """
1862
1863     slave = configure_pif(get_vlan_slave_of_pif(pif))
1864     slave.close()
1865
1866     f = open_pif_ifcfg(pif)
1867     f.write("VLAN=yes\n")
1868     f.attach_child(slave)
1869     
1870     return f
1871
1872 def configure_pif(pif):
1873     """Write the configuration for a PIF object.
1874
1875     Writes the configuration file the PIF and all dependent
1876     interfaces (bond slaves and VLAN masters etc).
1877
1878     Returns the open file handle for the interface configuration file.
1879     """
1880
1881     pifrec = db.get_pif_record(pif)
1882
1883     if pifrec['VLAN'] != '-1':
1884         f = configure_vlan_interface(pif)
1885     elif len(pifrec['bond_master_of']) != 0:
1886         f = configure_bond_interface(pif)
1887     else:
1888         f = configure_physical_interface(pif)
1889
1890     bridge = bridge_name(pif)
1891     if bridge:
1892         f.write("BRIDGE=%s\n" % bridge)
1893
1894     return f
1895
1896 def unconfigure_pif(pif):
1897     """Clear up the files created by configure_pif"""
1898     f = open_pif_ifcfg(pif)
1899     log("Unlinking stale file %s" % f.path())
1900     f.unlink()
1901     return f
1902 \f
1903 if __name__ == "__main__":
1904     rc = 1
1905     try:
1906         rc = main()
1907     except:
1908         ex = sys.exc_info()
1909         err = traceback.format_exception(*ex)
1910         for exline in err:
1911             log(exline)
1912
1913     if not debug_mode():
1914         syslog.closelog()
1915         
1916     sys.exit(rc)