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