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