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