1 # Copyright (c) 2008,2009 Citrix Systems, Inc.
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU Lesser General Public License as published
5 # by the Free Software Foundation; version 2.1 only. with the special
6 # exception on linking described in file LICENSE.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU Lesser General Public License for more details.
16 from xml.dom.minidom import getDOMImplementation
17 from xml.dom.minidom import parse as parseXML
30 class Error(Exception):
31 def __init__(self, msg):
32 Exception.__init__(self)
36 # Run external utilities
39 def run_command(command):
40 log("Running command: " + ' '.join(command))
41 rc = os.spawnl(os.P_WAIT, command[0], *command)
43 log("Command failed %d: " % rc + ' '.join(command))
48 # Configuration File Handling.
51 class ConfigurationFile(object):
52 """Write a file, tracking old and new versions.
54 Supports writing a new version of a file and applying and
55 reverting those changes.
58 __STATE = {"OPEN":"OPEN",
59 "NOT-APPLIED":"NOT-APPLIED", "APPLIED":"APPLIED",
60 "REVERTED":"REVERTED", "COMMITTED": "COMMITTED"}
62 def __init__(self, path):
63 dirname,basename = os.path.split(path)
65 self.__state = self.__STATE['OPEN']
68 self.__path = os.path.join(dirname, basename)
69 self.__oldpath = os.path.join(dirname, "." + basename + ".xapi-old")
70 self.__newpath = os.path.join(dirname, "." + basename + ".xapi-new")
72 self.__f = open(self.__newpath, "w")
74 def attach_child(self, child):
75 self.__children.append(child)
82 return open(self.path()).readlines()
86 def write(self, args):
87 if self.__state != self.__STATE['OPEN']:
88 raise Error("Attempt to write to file in state %s" % self.__state)
92 if self.__state != self.__STATE['OPEN']:
93 raise Error("Attempt to close file in state %s" % self.__state)
96 self.__state = self.__STATE['NOT-APPLIED']
99 if self.__state != self.__STATE['NOT-APPLIED']:
100 raise Error("Attempt to compare file in state %s" % self.__state)
105 if self.__state != self.__STATE['NOT-APPLIED']:
106 raise Error("Attempt to apply configuration from state %s" % self.__state)
108 for child in self.__children:
111 log("Applying changes to %s configuration" % self.__path)
113 # Remove previous backup.
114 if os.access(self.__oldpath, os.F_OK):
115 os.unlink(self.__oldpath)
117 # Save current configuration.
118 if os.access(self.__path, os.F_OK):
119 os.link(self.__path, self.__oldpath)
120 os.unlink(self.__path)
122 # Apply new configuration.
123 assert(os.path.exists(self.__newpath))
124 os.link(self.__newpath, self.__path)
126 # Remove temporary file.
127 os.unlink(self.__newpath)
129 self.__state = self.__STATE['APPLIED']
132 if self.__state != self.__STATE['APPLIED']:
133 raise Error("Attempt to revert configuration from state %s" % self.__state)
135 for child in self.__children:
138 log("Reverting changes to %s configuration" % self.__path)
140 # Remove existing new configuration
141 if os.access(self.__newpath, os.F_OK):
142 os.unlink(self.__newpath)
144 # Revert new configuration.
145 if os.access(self.__path, os.F_OK):
146 os.link(self.__path, self.__newpath)
147 os.unlink(self.__path)
149 # Revert to old configuration.
150 if os.access(self.__oldpath, os.F_OK):
151 os.link(self.__oldpath, self.__path)
152 os.unlink(self.__oldpath)
154 # Leave .*.xapi-new as an aid to debugging.
156 self.__state = self.__STATE['REVERTED']
159 if self.__state != self.__STATE['APPLIED']:
160 raise Error("Attempt to commit configuration from state %s" % self.__state)
162 for child in self.__children:
165 log("Committing changes to %s configuration" % self.__path)
167 if os.access(self.__oldpath, os.F_OK):
168 os.unlink(self.__oldpath)
169 if os.access(self.__newpath, os.F_OK):
170 os.unlink(self.__newpath)
172 self.__state = self.__STATE['COMMITTED']
175 # Helper functions for encoding/decoding database attributes to/from XML.
178 def _str_to_xml(xml, parent, tag, val):
179 e = xml.createElement(tag)
180 parent.appendChild(e)
181 v = xml.createTextNode(val)
183 def _str_from_xml(n):
184 def getText(nodelist):
186 for node in nodelist:
187 if node.nodeType == node.TEXT_NODE:
190 return getText(n.childNodes).strip()
192 def _bool_to_xml(xml, parent, tag, val):
194 _str_to_xml(xml, parent, tag, "True")
196 _str_to_xml(xml, parent, tag, "False")
197 def _bool_from_xml(n):
204 raise Error("Unknown boolean value %s" % s)
206 def _strlist_to_xml(xml, parent, ltag, itag, val):
207 e = xml.createElement(ltag)
208 parent.appendChild(e)
210 c = xml.createElement(itag)
212 cv = xml.createTextNode(v)
214 def _strlist_from_xml(n, ltag, itag):
216 for n in n.childNodes:
217 if n.nodeName == itag:
218 ret.append(_str_from_xml(n))
221 def _otherconfig_to_xml(xml, parent, val, attrs):
222 otherconfig = xml.createElement("other_config")
223 parent.appendChild(otherconfig)
224 for n,v in val.items():
226 raise Error("Unknown other-config attribute: %s" % n)
227 _str_to_xml(xml, otherconfig, n, v)
228 def _otherconfig_from_xml(n, attrs):
230 for n in n.childNodes:
231 if n.nodeName in attrs:
232 ret[n.nodeName] = _str_from_xml(n)
236 # Definitions of the database objects (and their attributes) used by interface-reconfigure.
238 # Each object is defined by a dictionary mapping an attribute name in
239 # the xapi database to a tuple containing two items:
240 # - a function which takes this attribute and encodes it as XML.
241 # - a function which takes XML and decocdes it into a value.
243 # other-config attributes are specified as a simple array of strings
246 _VLAN_XML_TAG = "vlan"
247 _BOND_XML_TAG = "bond"
248 _NETWORK_XML_TAG = "network"
250 _ETHTOOL_OTHERCONFIG_ATTRS = ['ethtool-%s' % x for x in 'autoneg', 'speed', 'duplex', 'rx', 'tx', 'sg', 'tso', 'ufo', 'gso' ]
252 _PIF_OTHERCONFIG_ATTRS = [ 'domain', 'peerdns', 'defaultroute', 'mtu', 'static-routes' ] + \
253 [ 'bond-%s' % x for x in 'mode', 'miimon', 'downdelay', 'updelay', 'use_carrier' ] + \
254 _ETHTOOL_OTHERCONFIG_ATTRS
256 _PIF_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
257 'management': (_bool_to_xml,_bool_from_xml),
258 'network': (_str_to_xml,_str_from_xml),
259 'device': (_str_to_xml,_str_from_xml),
260 'bond_master_of': (lambda x, p, t, v: _strlist_to_xml(x, p, 'bond_master_of', 'slave', v),
261 lambda n: _strlist_from_xml(n, 'bond_master_of', 'slave')),
262 'bond_slave_of': (_str_to_xml,_str_from_xml),
263 'VLAN': (_str_to_xml,_str_from_xml),
264 'VLAN_master_of': (_str_to_xml,_str_from_xml),
265 'VLAN_slave_of': (lambda x, p, t, v: _strlist_to_xml(x, p, 'VLAN_slave_of', 'master', v),
266 lambda n: _strlist_from_xml(n, 'VLAN_slave_Of', 'master')),
267 'ip_configuration_mode': (_str_to_xml,_str_from_xml),
268 'IP': (_str_to_xml,_str_from_xml),
269 'netmask': (_str_to_xml,_str_from_xml),
270 'gateway': (_str_to_xml,_str_from_xml),
271 'DNS': (_str_to_xml,_str_from_xml),
272 'MAC': (_str_to_xml,_str_from_xml),
273 'other_config': (lambda x, p, t, v: _otherconfig_to_xml(x, p, v, _PIF_OTHERCONFIG_ATTRS),
274 lambda n: _otherconfig_from_xml(n, _PIF_OTHERCONFIG_ATTRS)),
276 # Special case: We write the current value
277 # PIF.currently-attached to the cache but since it will
278 # not be valid when we come to use the cache later
279 # (i.e. after a reboot) we always read it as False.
280 'currently_attached': (_bool_to_xml, lambda n: False),
283 _VLAN_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
284 'tagged_PIF': (_str_to_xml,_str_from_xml),
285 'untagged_PIF': (_str_to_xml,_str_from_xml),
288 _BOND_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
289 'master': (_str_to_xml,_str_from_xml),
290 'slaves': (lambda x, p, t, v: _strlist_to_xml(x, p, 'slaves', 'slave', v),
291 lambda n: _strlist_from_xml(n, 'slaves', 'slave')),
294 _NETWORK_OTHERCONFIG_ATTRS = [ 'mtu', 'static-routes' ] + _ETHTOOL_OTHERCONFIG_ATTRS
296 _NETWORK_ATTRS = { 'uuid': (_str_to_xml,_str_from_xml),
297 'bridge': (_str_to_xml,_str_from_xml),
298 'PIFs': (lambda x, p, t, v: _strlist_to_xml(x, p, 'PIFs', 'PIF', v),
299 lambda n: _strlist_from_xml(n, 'PIFs', 'PIF')),
300 'other_config': (lambda x, p, t, v: _otherconfig_to_xml(x, p, v, _NETWORK_OTHERCONFIG_ATTRS),
301 lambda n: _otherconfig_from_xml(n, _NETWORK_OTHERCONFIG_ATTRS)),
305 # Database Cache object
311 assert(_db is not None)
314 def db_init_from_cache(cache):
317 _db = DatabaseCache(cache_file=cache)
319 def db_init_from_xenapi(session):
322 _db = DatabaseCache(session_ref=session)
324 class DatabaseCache(object):
325 def __read_xensource_inventory(self):
326 filename = "/etc/xensource-inventory"
327 f = open(filename, "r")
328 lines = [x.strip("\n") for x in f.readlines()]
331 defs = [ (l[:l.find("=")], l[(l.find("=") + 1):]) for l in lines ]
332 defs = [ (a, b.strip("'")) for (a,b) in defs ]
336 def __pif_on_host(self,pif):
337 return self.__pifs.has_key(pif)
339 def __get_pif_records_from_xapi(self, session, host):
341 for (p,rec) in session.xenapi.PIF.get_all_records().items():
342 if rec['host'] != host:
346 self.__pifs[p][f] = rec[f]
347 self.__pifs[p]['other_config'] = {}
348 for f in _PIF_OTHERCONFIG_ATTRS:
349 if not rec['other_config'].has_key(f): continue
350 self.__pifs[p]['other_config'][f] = rec['other_config'][f]
352 def __get_vlan_records_from_xapi(self, session):
354 for v in session.xenapi.VLAN.get_all():
355 rec = session.xenapi.VLAN.get_record(v)
356 if not self.__pif_on_host(rec['untagged_PIF']):
359 for f in _VLAN_ATTRS:
360 self.__vlans[v][f] = rec[f]
362 def __get_bond_records_from_xapi(self, session):
364 for b in session.xenapi.Bond.get_all():
365 rec = session.xenapi.Bond.get_record(b)
366 if not self.__pif_on_host(rec['master']):
369 for f in _BOND_ATTRS:
370 self.__bonds[b][f] = rec[f]
372 def __get_network_records_from_xapi(self, session):
374 for n in session.xenapi.network.get_all():
375 rec = session.xenapi.network.get_record(n)
376 self.__networks[n] = {}
377 for f in _NETWORK_ATTRS:
379 # drop PIFs on other hosts
380 self.__networks[n][f] = [p for p in rec[f] if self.__pif_on_host(p)]
382 self.__networks[n][f] = rec[f]
383 self.__networks[n]['other_config'] = {}
384 for f in _NETWORK_OTHERCONFIG_ATTRS:
385 if not rec['other_config'].has_key(f): continue
386 self.__networks[n]['other_config'][f] = rec['other_config'][f]
388 def __to_xml(self, xml, parent, key, ref, rec, attrs):
389 """Encode a database object as XML"""
390 e = xml.createElement(key)
391 parent.appendChild(e)
393 e.setAttribute('ref', ref)
395 for n,v in rec.items():
400 raise Error("Unknown attribute %s" % n)
401 def __from_xml(self, e, attrs):
402 """Decode a database object from XML"""
403 ref = e.attributes['ref'].value
405 for n in e.childNodes:
406 if n.nodeName in attrs:
407 _,h = attrs[n.nodeName]
408 rec[n.nodeName] = h(n)
411 def __init__(self, session_ref=None, cache_file=None):
412 if session_ref and cache_file:
413 raise Error("can't specify session reference and cache file")
414 if cache_file == None:
416 session = XenAPI.xapi_local()
419 log("No session ref given on command line, logging in.")
420 session.xenapi.login_with_password("root", "")
422 session._session = session_ref
426 inventory = self.__read_xensource_inventory()
427 assert(inventory.has_key('INSTALLATION_UUID'))
428 log("host uuid is %s" % inventory['INSTALLATION_UUID'])
430 host = session.xenapi.host.get_by_uuid(inventory['INSTALLATION_UUID'])
432 self.__get_pif_records_from_xapi(session, host)
434 self.__get_vlan_records_from_xapi(session)
435 self.__get_bond_records_from_xapi(session)
436 self.__get_network_records_from_xapi(session)
439 session.xenapi.session.logout()
441 log("Loading xapi database cache from %s" % cache_file)
443 xml = parseXML(cache_file)
450 assert(len(xml.childNodes) == 1)
451 toplevel = xml.childNodes[0]
453 assert(toplevel.nodeName == "xenserver-network-configuration")
455 for n in toplevel.childNodes:
456 if n.nodeName == "#text":
458 elif n.nodeName == _PIF_XML_TAG:
459 (ref,rec) = self.__from_xml(n, _PIF_ATTRS)
460 self.__pifs[ref] = rec
461 elif n.nodeName == _BOND_XML_TAG:
462 (ref,rec) = self.__from_xml(n, _BOND_ATTRS)
463 self.__bonds[ref] = rec
464 elif n.nodeName == _VLAN_XML_TAG:
465 (ref,rec) = self.__from_xml(n, _VLAN_ATTRS)
466 self.__vlans[ref] = rec
467 elif n.nodeName == _NETWORK_XML_TAG:
468 (ref,rec) = self.__from_xml(n, _NETWORK_ATTRS)
469 self.__networks[ref] = rec
471 raise Error("Unknown XML element %s" % n.nodeName)
473 def save(self, cache_file):
475 xml = getDOMImplementation().createDocument(
476 None, "xenserver-network-configuration", None)
477 for (ref,rec) in self.__pifs.items():
478 self.__to_xml(xml, xml.documentElement, _PIF_XML_TAG, ref, rec, _PIF_ATTRS)
479 for (ref,rec) in self.__bonds.items():
480 self.__to_xml(xml, xml.documentElement, _BOND_XML_TAG, ref, rec, _BOND_ATTRS)
481 for (ref,rec) in self.__vlans.items():
482 self.__to_xml(xml, xml.documentElement, _VLAN_XML_TAG, ref, rec, _VLAN_ATTRS)
483 for (ref,rec) in self.__networks.items():
484 self.__to_xml(xml, xml.documentElement, _NETWORK_XML_TAG, ref, rec,
487 f = open(cache_file, 'w')
488 f.write(xml.toprettyxml())
491 def get_pif_by_uuid(self, uuid):
492 pifs = map(lambda (ref,rec): ref,
493 filter(lambda (ref,rec): uuid == rec['uuid'],
494 self.__pifs.items()))
496 raise Error("Unknown PIF \"%s\"" % uuid)
498 raise Error("Non-unique PIF \"%s\"" % uuid)
502 def get_pifs_by_device(self, device):
503 return map(lambda (ref,rec): ref,
504 filter(lambda (ref,rec): rec['device'] == device,
505 self.__pifs.items()))
507 def get_pif_by_bridge(self, bridge):
508 networks = map(lambda (ref,rec): ref,
509 filter(lambda (ref,rec): rec['bridge'] == bridge,
510 self.__networks.items()))
511 if len(networks) == 0:
512 raise Error("No matching network \"%s\"" % bridge)
515 for network in networks:
516 nwrec = self.get_network_record(network)
517 for pif in nwrec['PIFs']:
518 pifrec = self.get_pif_record(pif)
520 raise Error("Multiple PIFs on host for network %s" % (bridge))
523 raise Error("No PIF on host for network %s" % (bridge))
526 def get_pif_record(self, pif):
527 if self.__pifs.has_key(pif):
528 return self.__pifs[pif]
529 raise Error("Unknown PIF \"%s\"" % pif)
530 def get_all_pifs(self):
532 def pif_exists(self, pif):
533 return self.__pifs.has_key(pif)
535 def get_management_pif(self):
536 """ Returns the management pif on host
538 all = self.get_all_pifs()
540 pifrec = self.get_pif_record(pif)
541 if pifrec['management']: return pif
544 def get_network_record(self, network):
545 if self.__networks.has_key(network):
546 return self.__networks[network]
547 raise Error("Unknown network \"%s\"" % network)
549 def get_bond_record(self, bond):
550 if self.__bonds.has_key(bond):
551 return self.__bonds[bond]
555 def get_vlan_record(self, vlan):
556 if self.__vlans.has_key(vlan):
557 return self.__vlans[vlan]
565 def ethtool_settings(oc):
567 if oc.has_key('ethtool-speed'):
568 val = oc['ethtool-speed']
569 if val in ["10", "100", "1000"]:
570 settings += ['speed', val]
572 log("Invalid value for ethtool-speed = %s. Must be 10|100|1000." % val)
573 if oc.has_key('ethtool-duplex'):
574 val = oc['ethtool-duplex']
575 if val in ["10", "100", "1000"]:
576 settings += ['duplex', 'val']
578 log("Invalid value for ethtool-duplex = %s. Must be half|full." % val)
579 if oc.has_key('ethtool-autoneg'):
580 val = oc['ethtool-autoneg']
581 if val in ["true", "on"]:
582 settings += ['autoneg', 'on']
583 elif val in ["false", "off"]:
584 settings += ['autoneg', 'off']
586 log("Invalid value for ethtool-autoneg = %s. Must be on|true|off|false." % val)
588 for opt in ("rx", "tx", "sg", "tso", "ufo", "gso"):
589 if oc.has_key("ethtool-" + opt):
590 val = oc["ethtool-" + opt]
591 if val in ["true", "on"]:
592 offload += [opt, 'on']
593 elif val in ["false", "off"]:
594 offload += [opt, 'off']
596 log("Invalid value for ethtool-%s = %s. Must be on|true|off|false." % (opt, val))
597 return settings,offload
600 if oc.has_key('mtu'):
602 int(oc['mtu']) # Check that the value is an integer
604 except ValueError, x:
605 log("Invalid value for mtu = %s" % oc['mtu'])
609 # IP Network Devices -- network devices with IP configuration
611 def pif_ipdev_name(pif):
612 """Return the ipdev name associated with pif"""
613 pifrec = db().get_pif_record(pif)
614 nwrec = db().get_network_record(pifrec['network'])
617 # TODO: sanity check that nwrec['bridgeless'] != 'true'
618 return nwrec['bridge']
620 # TODO: sanity check that nwrec['bridgeless'] == 'true'
621 return pif_netdev_name(pif)
624 # Bare Network Devices -- network devices without IP configuration
627 def netdev_exists(netdev):
628 return os.path.exists("/sys/class/net/" + netdev)
630 def pif_netdev_name(pif):
631 """Get the netdev name for a PIF."""
633 pifrec = db().get_pif_record(pif)
636 return "%(device)s.%(VLAN)s" % pifrec
638 return pifrec['device']
644 def pif_is_bridged(pif):
645 pifrec = db().get_pif_record(pif)
646 nwrec = db().get_network_record(pifrec['network'])
649 # TODO: sanity check that nwrec['bridgeless'] != 'true'
652 # TODO: sanity check that nwrec['bridgeless'] == 'true'
655 def pif_bridge_name(pif):
656 """Return the bridge name of a pif.
658 PIF must be a bridged PIF."""
659 pifrec = db().get_pif_record(pif)
661 nwrec = db().get_network_record(pifrec['network'])
664 return nwrec['bridge']
666 raise Error("PIF %(uuid)s does not have a bridge name" % pifrec)
671 def pif_is_bond(pif):
672 pifrec = db().get_pif_record(pif)
674 return len(pifrec['bond_master_of']) > 0
676 def pif_get_bond_masters(pif):
677 """Returns a list of PIFs which are bond masters of this PIF"""
679 pifrec = db().get_pif_record(pif)
681 bso = pifrec['bond_slave_of']
683 # bond-slave-of is currently a single reference but in principle a
684 # PIF could be a member of several bonds which are not
685 # concurrently attached. Be robust to this possibility.
686 if not bso or bso == "OpaqueRef:NULL":
688 elif not type(bso) == list:
691 bondrecs = [db().get_bond_record(bond) for bond in bso]
692 bondrecs = [rec for rec in bondrecs if rec]
694 return [bond['master'] for bond in bondrecs]
696 def pif_get_bond_slaves(pif):
697 """Returns a list of PIFs which make up the given bonded pif."""
699 pifrec = db().get_pif_record(pif)
701 bmo = pifrec['bond_master_of']
703 raise Error("Bond-master-of contains too many elements")
708 bondrec = db().get_bond_record(bmo[0])
710 raise Error("No bond record for bond master PIF")
712 return bondrec['slaves']
718 def pif_is_vlan(pif):
719 return db().get_pif_record(pif)['VLAN'] != '-1'
721 def pif_get_vlan_slave(pif):
722 """Find the PIF which is the VLAN slave of pif.
724 Returns the 'physical' PIF underneath the a VLAN PIF @pif."""
726 pifrec = db().get_pif_record(pif)
728 vlan = pifrec['VLAN_master_of']
729 if not vlan or vlan == "OpaqueRef:NULL":
730 raise Error("PIF is not a VLAN master")
732 vlanrec = db().get_vlan_record(vlan)
734 raise Error("No VLAN record found for PIF")
736 return vlanrec['tagged_PIF']
738 def pif_get_vlan_masters(pif):
739 """Returns a list of PIFs which are VLANs on top of the given pif."""
741 pifrec = db().get_pif_record(pif)
742 vlans = [db().get_vlan_record(v) for v in pifrec['VLAN_slave_of']]
743 return [v['untagged_PIF'] for v in vlans if v and db().pif_exists(v['untagged_PIF'])]
746 # Datapath base class
749 class Datapath(object):
750 """Object encapsulating the actions necessary to (de)configure the
751 datapath for a given PIF. Does not include configuration of the
752 IP address on the ipdev.
755 def __init__(self, pif):
758 def configure_ipdev(self, cfg):
759 """Write ifcfg TYPE field for an IPdev, plus any type specific
762 raise NotImplementedError
764 def preconfigure(self, parent):
765 """Prepare datapath configuration for PIF, but do not actually
768 Any configuration files should be attached to parent.
770 raise NotImplementedError
772 def bring_down_existing(self):
773 """Tear down any existing network device configuration which
774 needs to be undone in order to bring this PIF up.
776 raise NotImplementedError
779 """Apply the configuration prepared in the preconfigure stage.
781 Should assume any configuration files changed attached in
782 the preconfigure stage are applied and bring up the
783 necesary devices to provide the datapath for the
786 Should not bring up the IPdev.
788 raise NotImplementedError
791 """Called after the IPdev has been brought up.
793 Should do any final setup, including reinstating any
794 devices which were taken down in the bring_down_existing
797 raise NotImplementedError
799 def bring_down(self):
800 """Tear down and deconfigure the datapath. Should assume the
801 IPdev has already been brought down.
803 raise NotImplementedError
805 def DatapathFactory(pif):
806 # XXX Need a datapath object for bridgeless PIFs
809 network_conf = open("/etc/xensource/network.conf", 'r')
810 network_backend = network_conf.readline().strip()
813 raise Error("failed to determine network backend:" + e)
815 if network_backend == "bridge":
816 from InterfaceReconfigureBridge import DatapathBridge
817 return DatapathBridge(pif)
818 elif network_backend == "vswitch":
819 from InterfaceReconfigureVswitch import DatapathVswitch
820 return DatapathVswitch(pif)
822 raise Error("unknown network backend %s" % network_backend)