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