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