[node-patches] Change in ovirt-node[master]: [DRAFT] network: Rework runtime network model

fabiand at fedoraproject.org fabiand at fedoraproject.org
Thu Jun 6 13:13:31 UTC 2013


Fabian Deutsch has uploaded a new change for review.

Change subject: [DRAFT] network: Rework runtime network model
......................................................................

[DRAFT] network: Rework runtime network model

Previously there were strong assumptions about the runtime env, they are
relaxed now. The code is able to parse more complex setups more generic.

Bug-Url: https://bugzilla.redhat.com/show_bug.cgi?id=971410
Change-Id: I8053ea21af5029190bc16b89ee109d2d7937a05f
Signed-off-by: Fabian Deutsch <fabiand at fedoraproject.org>
---
M src/ovirt/node/base.py
M src/ovirt/node/config/defaults.py
M src/ovirt/node/config/network.py
M src/ovirt/node/setup/cim/cim_model.py
M src/ovirt/node/setup/core/network_page.py
M src/ovirt/node/setup/snmp/snmp_model.py
M src/ovirt/node/utils/fs.py
M src/ovirt/node/utils/network.py
8 files changed, 700 insertions(+), 529 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/ovirt-node refs/changes/11/15411/1

diff --git a/src/ovirt/node/base.py b/src/ovirt/node/base.py
index 5d6d12f..6d62b1e 100644
--- a/src/ovirt/node/base.py
+++ b/src/ovirt/node/base.py
@@ -52,6 +52,14 @@
         return [(k, v) for k, v in self.__dict__.items()
                 if isinstance(v, Base.Signal)]
 
+    def build_str(self, attributes=[], additional_pairs={}, name=None):
+        name = name or self.__class__.__name__
+        attrs = dict((k, self.__dict__[k]) for k in attributes)
+        attrs.update(additional_pairs)
+        attrs = " ".join(["%s='%s'" % i for i in sorted(attrs.items())])
+        addr = hex(id(self))
+        return ("<%s>" % " ".join(v for v in [name, attrs, "at", addr] if v))
+
     class Signal(object):
         """A convenience class for easier access to signals
         """
diff --git a/src/ovirt/node/config/defaults.py b/src/ovirt/node/config/defaults.py
index 1d7d54d..246fc51 100644
--- a/src/ovirt/node/config/defaults.py
+++ b/src/ovirt/node/config/defaults.py
@@ -19,7 +19,8 @@
 # MA  02110-1301, USA.  A copy of the GNU General Public License is
 # also available at http://www.gnu.org/copyleft/gpl.html.
 from ovirt.node import base, exceptions, valid, utils, config
-from ovirt.node.utils import fs, storage
+from ovirt.node.utils import storage, process
+from ovirt.node.utils.fs import ShellVarFile
 import glob
 import logging
 import os
@@ -45,101 +46,23 @@
 OVIRT_NODE_DEFAULTS_FILENAME = "/etc/default/ovirt"
 
 
-class SimpleProvider(base.Base):
-    """SimpleProvider writes simple KEY=VALUE (shell-like) configuration file
-
-    >>> fn = "/tmp/cfg_dummy.simple"
-    >>> open(fn, "w").close()
-    >>> cfg = {
-    ... "IP_ADDR": "127.0.0.1",
-    ... "NETMASK": "255.255.255.0",
-    ... }
-    >>> p = SimpleProvider(fn)
-    >>> p.get_dict()
-    {}
-    >>> p.update(cfg, True)
-    >>> p.get_dict() == cfg
-    True
+def exists():
+    """Determin if the defaults file exists
     """
-    def __init__(self, filename):
-        super(SimpleProvider, self).__init__()
-        self.filename = filename
-        if not os.path.exists(self.filename):
-            raise RuntimeError("File does not exist: %s" % self.filename)
-        self.logger.debug("Using %s" % self.filename)
-
-    def update(self, new_dict, remove_empty):
-        cfg = self.get_dict()
-        cfg.update(new_dict)
-
-        for key, value in cfg.items():
-            if remove_empty and value is None:
-                del cfg[key]
-            if value is not None and type(value) not in [str, unicode]:
-                raise TypeError("The type (%s) of %s is not allowed" %
-                                (type(value), key))
-        self._write(cfg)
-
-    def get_dict(self):
-        with open(self.filename) as source:
-            cfg = self._parse_dict(source)
-        return cfg
-
-    def _parse_dict(self, source):
-        """Parse a simple shell-var-style lines into a dict:
-
-        >>> import StringIO
-        >>> txt = "# A comment\\n"
-        >>> txt += "A=ah\\n"
-        >>> txt += "B=beh\\n"
-        >>> txt += "C=\\"ceh\\"\\n"
-        >>> txt += "D=\\"more=less\\"\\n"
-        >>> p = SimpleProvider("/tmp/cfg_dummy")
-        >>> sorted(p._parse_dict(StringIO.StringIO(txt)).items())
-        [('A', 'ah'), ('B', 'beh'), ('C', 'ceh'), ('D', 'more=less')]
-        """
-        cfg = {}
-        for line in source:
-                line = line.strip()
-                if line.startswith("#"):
-                    continue
-                try:
-                    key, value = line.split("=", 1)
-                    cfg[key] = value.strip("\"' \n")
-                except:
-                    self.logger.info("Failed to parse line: '%s'" % line)
-        return cfg
-
-    def _write(self, cfg):
-        lines = []
-        # Sort the dict, looks nicer
-        for key in sorted(cfg.iterkeys()):
-            lines.append("%s=\"%s\"" % (key, cfg[key]))
-        contents = "\n".join(lines) + "\n"
-
-        # The following logic is mainly needed to allow an "offline" testing
-        config_fs = fs.Config()
-        if config_fs.is_enabled():
-            with config_fs.open_file(self.filename, "w") as dst:
-                dst.write(contents)
-        else:
-            try:
-                fs.atomic_write(self.filename, contents)
-            except Exception as e:
-                self.logger.warning("Atomic write failed: %s" % e)
-                with open(self.filename, "w") as dst:
-                    dst.write(contents)
+    return os.path.exists(OVIRT_NODE_DEFAULTS_FILENAME)
 
 
-class ConfigFile(base.Base):
-    """ConfigFile is a specififc interface to some configuration file with a
-    specififc syntax
+class NodeConfigFile(base.Base):
+    """NodeConfigFile is a specififc interface to some configuration file
+    with a specififc syntax
     """
-    def __init__(self, filename=None, provider_class=None):
-        super(ConfigFile, self).__init__()
-        filename = filename or OVIRT_NODE_DEFAULTS_FILENAME
-        provider_class = provider_class or SimpleProvider
-        self.provider = provider_class(filename)
+    def __init__(self, filename=OVIRT_NODE_DEFAULTS_FILENAME):
+        super(NodeConfigFile, self).__init__()
+        if filename == OVIRT_NODE_DEFAULTS_FILENAME \
+           and not os.path.exists(filename):
+            raise RuntimeError("Node config file does not exist: %s" %
+                               filename)
+        self.provider = ShellVarFile(filename, create=True)
 
     def update(self, new_dict, remove_empty=False):
         """Reads /etc/defaults/ovirt and creates a dictionary
@@ -167,7 +90,7 @@
 
     def __init__(self, cfgfile=None):
         super(NodeConfigFileSection, self).__init__()
-        self.defaults = cfgfile or ConfigFile()
+        self.defaults = cfgfile or NodeConfigFile()
 
     def update(self, *args, **kwargs):
         """This function set's the correct entries in the defaults file for
@@ -295,8 +218,7 @@
     - OVIRT_IP_ADDRESS, OVIRT_IP_NETMASK, OVIRT_IP_GATEWAY
     - OVIRT_VLAN
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = Network(cfgfile)
     >>> n.update("eth0", "static", "10.0.0.1", "255.0.0.0", "10.0.0.255",
     ...          "20")
@@ -365,9 +287,10 @@
     def configure_no_networking(self, iface=None):
         """Can be used to disable all networking
         """
-        iface = iface or self.retrieve()["iface"]
-        name = iface + "-DISABLED"
-        self.update(name, None, None, None, None, None)
+        #iface = iface or self.retrieve()["iface"]
+        #name = iface + "-DISABLED"
+        # FIXME why should we use ifname-DISABLED here?
+        self.update(None, None, None, None, None, None)
 
     def configure_dhcp(self, iface, vlanid=None):
         """Can be used to configure NIC iface on the vlan vlanid with DHCP
@@ -380,6 +303,29 @@
         self.update(iface, "static", ipaddr, netmask, gateway, vlanid)
 
 
+class NetworkTopology(NodeConfigFileSection):
+    """Sets the network topology
+    - OVIRT_NETWORK_TOPOLOGY
+
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
+    >>> n = NetworkTopology(cfgfile)
+    >>> n.update("legacy")
+    >>> sorted(n.retrieve().items())
+    [('topology', 'legacy')]
+    """
+    keys = ("OVIRT_NETWORK_TOPOLOGY",)
+    known_topologies = ["legacy",
+                        # Legacy way, a bridge is created for BOOTIF
+
+                        "direct"
+                        # The BOOTIF NIC is configured directly
+                        ]
+
+    @NodeConfigFileSection.map_and_update_defaults_decorator
+    def update(self, topology="legacy"):
+        assert topology in self.known_topologies
+
+
 class IPv6(NodeConfigFileSection):
     """Sets IPv6 network stuff
     - OVIRT_IPV6 (static, auto, dhcp)
@@ -387,8 +333,7 @@
     - OVIRT_IPV6_NETMASK
     - OVIRT_IPV6_GATEWAY
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = IPv6(cfgfile)
     >>> n.update("auto", "11::22", "11::33", "11::44")
     >>> data = sorted(n.retrieve().items())
@@ -446,8 +391,7 @@
 
 class Hostname(NodeConfigFileSection):
     """Configure hostname
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> hostname = "host.example.com"
     >>> n = Hostname(cfgfile)
     >>> n.update(hostname)
@@ -496,8 +440,7 @@
 
 class Nameservers(NodeConfigFileSection):
     """Configure nameservers
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> servers = ["10.0.0.2", "10.0.0.3"]
     >>> n = Nameservers(cfgfile)
     >>> n.update(servers)
@@ -615,8 +558,7 @@
 class Timeservers(NodeConfigFileSection):
     """Configure timeservers
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> servers = ["10.0.0.4", "10.0.0.5", "0.example.com"]
     >>> n = Timeservers(cfgfile)
     >>> n.update(servers)
@@ -672,8 +614,7 @@
 class Syslog(NodeConfigFileSection):
     """Configure rsyslog
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> server = "10.0.0.6"
     >>> port = "514"
     >>> n = Syslog(cfgfile)
@@ -711,8 +652,7 @@
 class Collectd(NodeConfigFileSection):
     """Configure collectd
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> server = "10.0.0.7"
     >>> port = "42"
     >>> n = Collectd(cfgfile)
@@ -756,8 +696,7 @@
 class KDump(NodeConfigFileSection):
     """Configure kdump
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> nfs_url = "host.example.com:/dst/path"
     >>> ssh_url = "root at host.example.com"
     >>> n = KDump(cfgfile)
@@ -905,8 +844,7 @@
 class iSCSI(NodeConfigFileSection):
     """Configure iSCSI
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = iSCSI(cfgfile)
     >>> n.update("iqn.1992-01.com.example:node",
     ...          "iqn.1992-01.com.example:target", "10.0.0.8", "42")
@@ -948,8 +886,7 @@
 class Netconsole(NodeConfigFileSection):
     """Configure netconsole
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = Netconsole(cfgfile)
     >>> server = "10.0.0.9"
     >>> port = "666"
@@ -984,8 +921,7 @@
 class Logrotate(NodeConfigFileSection):
     """Configure logrotate
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = Logrotate(cfgfile)
     >>> max_size = "42"
     >>> n.update(max_size)
@@ -1018,8 +954,7 @@
 class Keyboard(NodeConfigFileSection):
     """Configure keyboard
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = Keyboard(cfgfile)
     >>> layout = "de_DE.UTF-8"
     >>> n.update(layout)
@@ -1056,8 +991,7 @@
 class NFSv4(NodeConfigFileSection):
     """Configure NFSv4
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = NFSv4(cfgfile)
     >>> domain = "foo.example"
     >>> n.update(domain)
@@ -1092,8 +1026,7 @@
 class SSH(NodeConfigFileSection):
     """Configure SSH
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = SSH(cfgfile)
     >>> pwauth = True
     >>> num_bytes = "24"
@@ -1162,8 +1095,7 @@
     """Configure storage
     This is a class to handle the storage parameters used at installation time
 
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = Installation(cfgfile)
     >>> kwargs = {"init": ["/dev/sda"], "root_install": "1"}
     >>> n.update(**kwargs)
diff --git a/src/ovirt/node/config/network.py b/src/ovirt/node/config/network.py
index 5a3a8e6..0f70a77 100644
--- a/src/ovirt/node/config/network.py
+++ b/src/ovirt/node/config/network.py
@@ -20,9 +20,10 @@
 # also available at http://www.gnu.org/copyleft/gpl.html.
 from ovirt.node import utils, base
 from ovirt.node.utils import AugeasWrapper as Augeas
+from ovirt.node.utils.fs import ShellVarFile
+import glob
 import logging
 import os
-import glob
 
 """
 Some convenience functions related to networking
@@ -32,61 +33,102 @@
 LOGGER = logging.getLogger(__name__)
 
 
-def iface(iface):
-    """Retuns the config of an iface
-
-    Args:
-        iface: Interface to retrieve the config for
-    Returns:
-        A dict of (nic-name, nic-infos-dict)
+class NicConfig(base.Base):
+    """A common interface to NIC configuration options
     """
-    LOGGER.debug("Getting configuration for '%s'" % iface)
-    Augeas.force_reload()
+    ifname = None
 
-    info = {}
+    type = None
+    bootproto = None
+    ipaddr = None
+    netmask = None
+    gateway = None
+    bridge = None
+    vlan = None
+    device = None
+    onboot = None
 
-    aug = Augeas()
-    filepath = "/etc/sysconfig/network-scripts/ifcfg-%s" % iface
-    augdevicepath = "/files%s" % filepath
+    ipv6init = None
+    ipv6forwarding = None
+    ipv6_autoconf = None
+    dhcpv6c = None
+    ipv6addr = None
+    ipv6_defaultgw = None
 
-    if not os.path.exists(filepath):
-        LOGGER.debug("No config file %s" % filepath)
+    vlan_parent = None
 
-    # Type
-    info["type"] = aug.get(augdevicepath + "/TYPE", True)
+    _backend = None
+    _keys = ["bridge", "type", "bootproto", "ipaddr", "netmask",
+             "gateway", "vlan", "device", "onboot", "hwaddr",
+             "ipv6init", "ipv6forwarding", "ipv6_autoconf",
+             "dhcpv6c", "ipv6addr", "ipv6_defaultgw"]
 
-    # Bootprotocol
-    info["bootproto"] = aug.get(augdevicepath + "/BOOTPROTO", True)
+    def __init__(self, ifname):
+        super(NicConfig, self).__init__()
+        self.ifname = ifname
+        self._backend = NicConfig.IfcfgBackend(self)
+        self.load()
 
-    # IPV4
-    for p in ["IPADDR", "NETMASK", "GATEWAY"]:
-        info[p.lower()] = aug.get(augdevicepath + "/" + p, True)
+    def exists(self):
+        """Return if a config exists
+        """
+        return self._backend.exists(self.ifname)
 
-    # FIXME IPv6
+    def load(self):
+        data = self._backend.read(self.ifname)
+        # Additional convenience stuff
+        self.vlan = self.vlan.strip() if self.vlan else self.vlan
+        if self.vlan:
+            parts = self.ifname.split(".")
+            self.vlan_id = parts[-1:][0]
+            self.vlan_parent = ".".join(parts[:-1])
 
-    # Parent bridge
-    info["bridge"] = aug.get(augdevicepath + "/BRIDGE", True)
+            self.logger.debug("Found VLAN %s on %s" %
+                              (str(self.vlan_id), self.ifname))
+        return data
 
-    # VLAN
-    info["is_vlan"] = aug.get(augdevicepath + "/VLAN", True) is not None
-    name_says_vlan = "." in iface
-    if info["is_vlan"] != name_says_vlan:
-        LOGGER.warning("NIC config says the device is a VLAN, but the name" +
-                       "doesn't reflect that: %s (%s vs %s)" % (iface,
-                       info["is_vlan"], name_says_vlan))
+    def save(self):
+        return self._backend.write(self.ifname)
 
-    if info["is_vlan"] is True:
-        parts = iface.split(".")
-        vlanid = parts[-1:][0]
-        info["vlanid"] = vlanid
-        info["vlan_parent"] = ".".join(parts[:-1])
+    def __str__(self):
+        return self.build_str("ifname")
 
-        info["type"] = "vlan"
-        LOGGER.debug("Found VLAN %s on %s" % (str(vlanid), iface))
-    else:
-        info["vlanid"] = None
+    class IfcfgBackend(base.Base):
+        filename_tpl = "/etc/sysconfig/network-scripts/ifcfg-%s"
 
-    return info
+        def __init__(self, cfg):
+            super(NicConfig.IfcfgBackend, self).__init__()
+            self.cfg = cfg
+
+        def exists(self, ifname):
+            """Return true if a config for ifname exists
+            """
+            return os.path.isfile(self.filename_tpl % ifname)
+
+        def read(self, ifname):
+            """Read values frmo a ifcfg file and update self.cfg
+            """
+            filename = self.filename_tpl % ifname
+
+            if not os.path.exists(filename):
+                self.logger.info("Config not found for %s" % ifname)
+                return
+
+            data = ShellVarFile(filename).get_dict()
+
+            for k in self.cfg._keys:
+                self.cfg.__dict__[k] = data.get(k.upper(), None)
+
+        def write(self, cfg):
+            """Write a ifcfg file from the cfg
+            """
+            assert type(cfg) is NicConfig
+            filename = self.filename_tpl % cfg.ifname
+            dst = ShellVarFile(filename)
+            data = {}
+            for k in self.cfg._keys:
+                data[k.upper()] = cfg.__dict__.get(k)
+            dst.update(data, True)
 
 
 def _aug_get_or_set(augpath, new_servers=None):
@@ -173,44 +215,6 @@
     return cfg_hostname
 
 
-class Ifcfg(base.Base):
-    """Object to access ifcfg-%ifname
-    """
-    bridge = None
-    type = None
-    bootproto = None
-    ipaddr = None
-    netmask = None
-    gateway = None
-    vlan = None
-    device = None
-    hwaddr = None
-    onboot = None
-
-    def __init__(self, iface):
-        self.iface = iface
-        self.aug = Augeas()
-        self.load_properties()
-
-    def load_properties(self):
-        Augeas.force_reload()
-        for p in ["bridge", "type", "bootproto", "ipaddr", "netmask",
-                  "gateway", "vlan", "device", "onboot", "hwaddr"]:
-            self.__dict__[p] = self.ifcfg_property(p.upper())
-
-    def ifcfg_property(self, name):
-        filepath = "/etc/sysconfig/network-scripts/ifcfg-%s" % self.iface
-        augdevicepath = "/files%s" % filepath
-
-        value = None
-        if os.path.exists(filepath):
-            value = self.aug.get("%s/%s" % (augdevicepath, name), True)
-        else:
-            LOGGER.debug("No config file %s" % filepath)
-
-        return value
-
-
 def ifaces():
     """Returns all configured ifaces
     """
@@ -220,32 +224,3 @@
         iface = fn[len(filepath):]
         ifaces.append(iface)
     return ifaces
-
-
-def node_bridge():
-    """Return the configured bridge
-
-    Returns:
-        Ifname of a configured bridge or None if none is configured
-    """
-    bridge = None
-    for iface in ifaces():
-        nic = Ifcfg(iface)
-        if nic.type == u"Bridge":
-            bridge = iface
-            break
-    return bridge
-
-
-def node_bridge_slave():
-    """Returns the interface which is the slave of the configfured bridge
-    """
-    slave = None
-    bridge = node_bridge()
-    if bridge:
-        for iface in ifaces():
-            nic = Ifcfg(iface)
-            if nic.bridge == bridge:
-                slave = iface
-                break
-    return slave
diff --git a/src/ovirt/node/setup/cim/cim_model.py b/src/ovirt/node/setup/cim/cim_model.py
index 72453b0..798aa0e 100644
--- a/src/ovirt/node/setup/cim/cim_model.py
+++ b/src/ovirt/node/setup/cim/cim_model.py
@@ -31,9 +31,8 @@
 class CIM(NodeConfigFileSection):
     """Configure CIM
 
-    >>> from ovirt.node.config.defaults import ConfigFile, SimpleProvider
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> from ovirt.node.config.defaults import NodeConfigFile
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = CIM(cfgfile)
     >>> n.update(True)
     >>> n.retrieve()
diff --git a/src/ovirt/node/setup/core/network_page.py b/src/ovirt/node/setup/core/network_page.py
index 4a7aedd..6b668af 100644
--- a/src/ovirt/node/setup/core/network_page.py
+++ b/src/ovirt/node/setup/core/network_page.py
@@ -21,8 +21,9 @@
 from ovirt.node import plugins, ui, valid, utils, config
 from ovirt.node.config import defaults
 from ovirt.node.plugins import Changeset
-from ovirt.node.utils import network
 from ovirt.node.setup.core import ping
+from ovirt.node.utils import network
+from ovirt.node.utils.network import BridgedNIC
 
 """
 Network page plugin
@@ -126,17 +127,20 @@
         return page
 
     def _get_nics(self):
-        justify = lambda txt, l: txt.ljust(l)[0:l]
+        def justify(txt, l):
+            txt = txt if txt else ""
+            return txt.ljust(l)[0:l]
         node_nics = []
         first_nic = None
-        for name, nic in sorted(utils.network.node_nics().items()):
+        model = utils.network.NodeNetwork()
+        for name, nic in sorted(model.nics().items()):
             if first_nic is None:
                 first_nic = name
-            bootproto = "Configured" if nic["bootproto"] else "Unconfigured"
-            description = " ".join([justify(nic["name"], 7),
-                                    justify(bootproto, 13),
-                                    justify(nic["vendor"], 14),
-                                    justify(nic["hwaddr"], 17)
+            is_cfg = "Configured" if nic.is_configured() else "Unconfigured"
+            description = " ".join([justify(nic.ifname, 7),
+                                    justify(is_cfg, 13),
+                                    justify(nic.vendor, 14),
+                                    justify(nic.hwaddr, 17)
                                     ])
             node_nics.append((name, description))
         return node_nics
@@ -216,8 +220,8 @@
             return
 
         if "dialog.nic.identify" in changes:
-            iface = self._model_extra["dialog.nic.iface"]
-            utils.network.NIC(iface).identify()
+            ifname = self._model_extra["dialog.nic.ifname"]
+            utils.network.NIC(ifname).identify()
             self.application.notice("Flashing lights now")
             return
 
@@ -275,7 +279,7 @@
                        ipv6_bootproto, ipv6_address, ipv6_netmask,
                        ipv6_gateway, vlanid):
         vlanid = vlanid or None
-        iface = self._model_extra["dialog.nic.iface"]
+        iface = self._model_extra["dialog.nic.ifname"]
 
         model = defaults.Network()
         ipv6model = defaults.IPv6()
@@ -332,64 +336,63 @@
 class NicDetailsDialog(ui.Dialog):
     plugin = None
 
-    def __init__(self, plugin, iface):
+    def __init__(self, plugin, ifname):
         super(NicDetailsDialog, self).__init__("dialog.nic",
-                                               "NIC Details: %s" % iface, [])
+                                               "NIC Details: %s" % ifname, [])
         self.plugin = plugin
 
         # Populate model with nic specific informations
-        self.logger.debug("Building NIC details dialog for %s" % iface)
+        self.logger.debug("Building NIC details dialog for %s" % ifname)
 
-        self.logger.debug("Getting informations for NIC details page")
-        live = utils.network.node_nics()[iface]
-        cfg = defaults.Network().retrieve()
-        ip6cfg = defaults.IPv6().retrieve()
+        nic = utils.network.NodeNetwork().nics()[ifname]
 
-        self.logger.debug("live: %s" % live)
-        self.logger.debug("cfg: %s" % cfg)
-        self.logger.debug("ipv6cfg: %s" % ip6cfg)
+        model = defaults.Network().retrieve()
+        ip6model = defaults.IPv6().retrieve()
 
-        # The primary interface of this Node:
-        node_bridge_slave = config.network.node_bridge_slave()
+        self.logger.debug("nic: %s" % nic)
+        self.logger.debug("model: %s" % model)
+        self.logger.debug("ip6model: %s" % ip6model)
 
-        if node_bridge_slave and node_bridge_slave != iface:
-            # The config contains the information for the primary iface,
-            # because this iface is not the primary iface we clear the config
-            cfg = dict((k, "") for k in cfg.keys())
+        is_primary_interface = model["iface"] == ifname
 
-        ipaddr, netmask, gateway, vlanid = (cfg["ipaddr"], cfg["netmask"],
-                                            cfg["gateway"], cfg["vlanid"])
+        if not is_primary_interface:
+            # The config contains the information for the primary ifnamee,
+            # because this ifnamee is not the primaryifnameme we clear the
+            # config
+            model = dict((k, "") for k in model.keys())
 
-        ip6addr, ip6netmask, ip6gateway, ip6bootproto = (ip6cfg["ipaddr"],
-                                                         ip6cfg["netmask"],
-                                                         ip6cfg["gateway"],
-                                                         ip6cfg["bootproto"])
+        ipaddr, netmask, gateway, vlanid = (model["ipaddr"], model["netmask"],
+                                            model["gateway"], model["vlanid"])
 
-        bridge_nic = utils.network.NIC(live["bridge"])
-        if cfg["bootproto"] == "dhcp":
-            if bridge_nic.exists():
-                routes = utils.network.Routes()
-                ipaddr, netmask = bridge_nic.ipv4_address().items()
-                gateway = routes.default()
-                vlanid = bridge_nic.vlanid()
-            else:
-                self.logger.warning("Bridge assigned but couldn't gather " +
-                                    "live info: %s" % bridge_nic)
+        ip6addr, ip6netmask, ip6gateway, ip6bootproto = (ip6model["ipaddr"],
+                                                         ip6model["netmask"],
+                                                         ip6model["gateway"],
+                                                         ip6model["bootproto"])
 
-        link_status_txt = ("Connected" if live["link_detected"]
+        if type(nic) is BridgedNIC:
+            if model["bootproto"] == "dhcp":
+                if nic.bridge_nic.exists():
+                    routes = utils.network.Routes()
+                    gateway = routes.default()
+                    ipaddr, netmask = nic.bridge_nic.ipv4_address().items()
+                    vlanid = ",".join(nic.bridge_nic.vlanids())
+                else:
+                    self.logger.warning("Bridge assigned but couldn't " +
+                                        "gather nic info: %s" % nic.bridge_nic)
+
+        link_status_txt = ("Connected" if nic.has_link()
                            else "Disconnected")
-
-        self.logger.debug("xxxxx bootporoto: %s" % cfg)
+        vendor_txt = nic.vendor[:24] if nic.vendor else ""
 
         self.plugin._model_extra.update({
-            "dialog.nic.iface": live["name"],
-            "dialog.nic.driver": live["driver"],
-            "dialog.nic.protocol": live["bootproto"] or "N/A",
-            "dialog.nic.vendor": live["vendor"][:24],
+            "dialog.nic.ifname": nic.ifname,
+            "dialog.nic.driver": nic.driver,
+            "dialog.nic.protocol": nic.config.bootproto or "N/A",
+            "dialog.nic.vendor": vendor_txt,
             "dialog.nic.link_status": link_status_txt,
-            "dialog.nic.hwaddress": live["hwaddr"],
+            "dialog.nic.hwaddress": nic.hwaddr,
 
-            "dialog.nic.ipv4.bootproto": cfg["bootproto"],
+            "dialog.nic.ipv4.bootproto": model["bootproto"],
             "dialog.nic.ipv4.address": ipaddr,
             "dialog.nic.ipv4.netmask": netmask,
             "dialog.nic.ipv4.gateway": gateway,
@@ -404,7 +407,8 @@
 
         padd = lambda l: l.ljust(12)
         ws = [ui.Row("dialog.nic._row[0]",
-                     [ui.KeywordLabel("dialog.nic.iface", padd("Interface: ")),
+                     [ui.KeywordLabel("dialog.nic.ifname",
+                                      padd("Interface: ")),
                       ui.KeywordLabel("dialog.nic.driver", padd("Driver: ")),
                       ]),
               ui.Row("dialog.nic._row[1]",
diff --git a/src/ovirt/node/setup/snmp/snmp_model.py b/src/ovirt/node/setup/snmp/snmp_model.py
index 5855e35..2ad6d5b 100644
--- a/src/ovirt/node/setup/snmp/snmp_model.py
+++ b/src/ovirt/node/setup/snmp/snmp_model.py
@@ -70,9 +70,8 @@
 class SNMP(NodeConfigFileSection):
     """Configure SNMP
 
-    >>> from ovirt.node.config.defaults import ConfigFile, SimpleProvider
-    >>> fn = "/tmp/cfg_dummy"
-    >>> cfgfile = ConfigFile(fn, SimpleProvider)
+    >>> from ovirt.node.config.defaults import NodeConfigFile
+    >>> cfgfile = NodeConfigFile("/tmp/cfg_dummy")
     >>> n = SNMP(cfgfile)
     >>> n.update("secret")
     >>> n.retrieve().items()
diff --git a/src/ovirt/node/utils/fs.py b/src/ovirt/node/utils/fs.py
index 429c9e8..580d543 100644
--- a/src/ovirt/node/utils/fs.py
+++ b/src/ovirt/node/utils/fs.py
@@ -215,3 +215,105 @@
 
     def open_file(self, filename, mode="r"):
         return open(self._config_path(filename), mode)
+
+
+class ShellVarFile(base.Base):
+    """ShellVarFile writes simple KEY=VALUE (shell-like) configuration file
+
+    >>> fn = "/tmp/cfg_dummy.simple"
+    >>> open(fn, "w").close()
+    >>> cfg = {
+    ... "IP_ADDR": "127.0.0.1",
+    ... "NETMASK": "255.255.255.0",
+    ... }
+    >>> p = ShellVarFile(fn)
+    >>> p.get_dict()
+    {}
+    >>> p.update(cfg, True)
+    >>> p.get_dict() == cfg
+    True
+    """
+    def __init__(self, filename, create=False):
+        super(ShellVarFile, self).__init__()
+        self.filename = filename
+        if not os.path.exists(self.filename):
+            if create:
+                self._write("")
+            else:
+                raise RuntimeError("File does not exist: %s" % self.filename)
+
+    def get_dict(self):
+        """Returns a dict of (key, value) pairs
+        """
+        with open(self.filename) as source:
+            cfg = self._parse_dict(source)
+        return cfg
+
+    def write(self, cfg, remove_empty=True):
+        for key, value in cfg.items():
+            if remove_empty and value is None:
+                del cfg[key]
+            if value is not None and type(value) not in [str, unicode]:
+                raise TypeError("The type (%s) of %s is not allowed" %
+                                (type(value), key))
+        lines = []
+        # Sort the dict, looks nicer
+        for key in sorted(cfg.iterkeys()):
+            lines.append("%s=\"%s\"" % (key, cfg[key]))
+        contents = "\n".join(lines) + "\n"
+        self._write(contents)
+
+    def update(self, new_dict, remove_empty):
+        """Update the file using new_dict
+        Keys not present in the dict, but present in the file will be kept in
+        the file.
+
+        Args:
+            new_dict: A dictionary containing the keys to be updated
+            remove_empty: Remove keys from file if their value in new_dict
+                          is None.
+                          If False then the keys will be added to the file
+                          without any value. (Aka flags)
+        """
+        cfg = self.get_dict()
+        cfg.update(new_dict)
+        self.write(cfg)
+
+    def _parse_dict(self, source):
+        """Parse a simple shell-var-style lines into a dict:
+
+        >>> import StringIO
+        >>> txt = "# A comment\\n"
+        >>> txt += "A=ah\\n"
+        >>> txt += "B=beh\\n"
+        >>> txt += "C=\\"ceh\\"\\n"
+        >>> txt += "D=\\"more=less\\"\\n"
+        >>> p = ShellVarFile("/tmp/cfg_dummy")
+        >>> sorted(p._parse_dict(StringIO.StringIO(txt)).items())
+        [('A', 'ah'), ('B', 'beh'), ('C', 'ceh'), ('D', 'more=less')]
+        """
+        cfg = {}
+        for line in source:
+                line = line.strip()
+                if line.startswith("#"):
+                    continue
+                try:
+                    key, value = line.split("=", 1)
+                    cfg[key] = value.strip("\"' \n")
+                except:
+                    self.logger.info("Failed to parse line: '%s'" % line)
+        return cfg
+
+    def _write(self, contents):
+        # The following logic is mainly needed to allow an "offline" testing
+        config_fs = Config()
+        if config_fs.is_enabled():
+            with config_fs.open_file(self.filename, "w") as dst:
+                dst.write(contents)
+        else:
+            try:
+                atomic_write(self.filename, contents)
+            except Exception as e:
+                self.logger.warning("Atomic write failed: %s" % e)
+                with open(self.filename, "w") as dst:
+                    dst.write(contents)
diff --git a/src/ovirt/node/utils/network.py b/src/ovirt/node/utils/network.py
index 3336f13..6513012 100644
--- a/src/ovirt/node/utils/network.py
+++ b/src/ovirt/node/utils/network.py
@@ -18,12 +18,13 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.  A copy of the GNU General Public License is
 # also available at http://www.gnu.org/copyleft/gpl.html.
-from ovirt.node import base, utils, config
+from ovirt.node import base, utils
+from ovirt.node.config import defaults
+from ovirt.node.config.network import NicConfig
 import glob
 import gudev
 import logging
 import os.path
-import ovirt.node.config.network
 import ovirt.node.utils.fs
 import ovirt.node.utils.process as process
 import re
@@ -82,222 +83,122 @@
     return _query_udev_ifaces()
 
 
-def is_configured():
-    """Determin if any NIC has been configured for/by Node
-
-    Returns:
-        True if any nic has been configured
+class UdevNICInfo(base.Base):
+    """Gather NIC infos form udev
     """
-    return any(key["bootproto"] is not None for key in node_nics().values())
+    _client = gudev.Client(['net'])
+    _device = None
+
+    ifname = None
+
+    def __init__(self, iface):
+        super(UdevNICInfo, self).__init__()
+        self.ifname = iface
+        for d in self._client.query_by_subsystem("net"):
+            if d.get_property("INTERFACE") == iface:
+                self._device = d
+
+        if not self._device:
+            raise UnknownNicError("udev has no infos for %s" % iface)
+
+    @property
+    def name(self):
+        return self._device.get_property("INTERFACE")
+
+    @property
+    def vendor(self):
+        return self._device.get_property("ID_VENDOR_FROM_DATABASE")
+
+    @property
+    def devtype(self):
+        return self._device.get_property("DEVTYPE")
+
+    @property
+    def devpath(self):
+        return self._device.get_property("DEVPATH")
 
 
-def iface_information(iface, with_slow=True):
-    """Retuns all system NICs (via udev)
-
-    FIXME move into NIC
-
-    Args:
-        nics: List of NIC names
-    Returns:
-        A dict of (nic-name, nic-infos-dict)
+class SysfsNICInfo(base.Base):
+    """Gather NIC infos fom sysfs
     """
-    info = None
+    ifname = None
 
-    client = gudev.Client(['net'])
-    for d in client.query_by_subsystem("net"):
-#        assert d.has_property("ID_VENDOR_FROM_DATABASE"), \
-#                "udev informations are incomplete (udevadm re-trigger?)"
+    def __init__(self, ifname):
+        super(SysfsNICInfo, self).__init__()
+        self.ifname = ifname
 
-        uinfo = {"name": d.get_property("INTERFACE"),
-                 "vendor": d.get_property("ID_VENDOR_FROM_DATABASE")
-                 or "unkown",
-                 "devtype": d.get_property("DEVTYPE") or "unknown",
-                 "devpath": d.get_property("DEVPATH")
-                 }
+    @property
+    def driver(self):
+        driver_symlink = "/sys/class/net/%s/device/driver" % self.ifname
+        driver = "unknown"
+        if os.path.islink(driver_symlink):
+            try:
+                driver = os.path.basename(os.readlink(driver_symlink))
+            except Exception as e:
+                self.logger.warning(("Exception %s while reading driver " +
+                                     "of '%s' from '%s'") % (e, self.ifname,
+                                                             driver_symlink))
+        return driver
 
-        if uinfo["name"] == iface:
-            info = uinfo
+    @property
+    def hwaddr(self):
+        #hwaddr = "unkown"
+        hwfilename = "/sys/class/net/%s/address" % self.ifname
+        hwaddr = ovirt.node.utils.fs.get_contents(hwfilename).strip()
+        return hwaddr
 
-    assert info, "Unknown nic %s" % iface
+    @property
+    def systype(self):
+        systype = "ethernet"
 
-    LOGGER.debug("Getting live information for '%s'" % iface)
+        if len(glob.glob("/proc/net/vlan/%s" % self.ifname)) > 0:
+            # Check if vlan
+            systype = "vlan"
 
-    # Driver
-    driver_symlink = "/sys/class/net/%s/device/driver" % iface
-    driver = "unknown"
-    if os.path.islink(driver_symlink):
-        try:
-            driver = os.path.basename(os.readlink(driver_symlink))
-        except Exception as e:
-            LOGGER.warning(("Exception %s while reading driver " +
-                            "of '%s' from '%s'") % (e, iface, driver_symlink))
-    info["driver"] = driver
+        elif os.path.exists("/sys/class/net/%s/bridge" % self.ifname):
+            # Check if bridge
+            systype = "bridge"
 
-    # Hwaddr
-    hwaddr = "unkown"
-    hwfilename = "/sys/class/net/%s/address" % iface
-    hwaddr = ovirt.node.utils.fs.get_contents(hwfilename).strip()
-    info["hwaddr"] = hwaddr
-
-    # Check bridge
-    if os.path.exists("/sys/class/net/%s/bridge" % iface):
-        info["type"] = "bridge"
-
-    # Check vlan
-    if len(glob.glob("/proc/net/vlan/%s" % iface)) > 0:
-        info["type"] = "vlan"
-
-    if "type" not in info:
-        devtype = info["devtype"]
-        LOGGER.warning(("Type of %s still unknown, using devtype " +
-                        "%s") % (iface, devtype))
-        info["type"] = devtype
-
-    if with_slow:
-        info.update(_slow_iface_information(iface))
-
-    return info
-
-
-def _slow_iface_information(iface):
-    info = {}
-
-    nic = NIC(iface)
-    # Current IP addresses
-    info["addresses"] = nic.ip_addresses()
-
-    # Current link state
-    info["link_detected"] = nic.has_link()
-
-    return info
-
-
-def relevant_ifaces(filter_bridges=True, filter_vlans=True):
-    """Retuns relevant system NICs (via udev)
-
-    Filters out
-    - loop
-    - bonds
-    - vnets
-    - bridges
-    - sit
-    - vlans
-
-    >>> "lo" in relevant_ifaces()
-    False
-
-    Args:
-        filter_bridges: If bridges shall be filtered out too
-        filter_vlans: If vlans shall be filtered out too
-    Returns:
-        List of strings, the NIC names
-    """
-    valid_name = lambda n: not (n == "lo" or
-                                n.startswith("bond") or
-                                n.startswith("sit") or
-                                n.startswith("vnet") or
-                                n.startswith("tun") or
-                                n.startswith("wlan") or
-                                n.startswith("virbr") or
-                                (filter_vlans and ("." in n)))
-# FIXME!!!
-#    valid_props = lambda i, p: (filter_bridges and (p["type"] != "bridge"))
-
-    relevant_ifaces = [iface for iface in all_ifaces() if valid_name(iface)]
-#    relevant_ifaces = {iface: iface_information(iface) for iface \
-#                       in relevant_ifaces \
-#                       if valid_props(iface, iface_information(iface, False))}
-
-    irrelevant_names = set(all_ifaces()) - set(relevant_ifaces)
-    LOGGER.debug("Irrelevant interfaces: %s" % irrelevant_names)
-    LOGGER.debug("Relevant interfaces: %s" % relevant_ifaces)
-
-    return relevant_ifaces
-
-
-def node_nics():
-    """Returns Node's NIC model.
-    This squashes nic, bridge and vlan informations.
-
-    All valid NICs of the system are returned, live and cfg informations merged
-    into one dict.
-    A NIC is "Configured" if itself or a vlan child is a member of a bridge.
-    If a NIC is configured, merge the info+cfg of the bridge into the slave.
-    If the slave is a vlan NIC set the vlanidof the parent device according to
-    this vlan NICs id.
-
-    >>> node_nics() != None
-    True
-    """
-    all_ifaces = relevant_ifaces(filter_bridges=False, filter_vlans=False)
-    all_infos = dict((i, iface_information(i)) for i in all_ifaces)
-    all_cfgs = dict((i, ovirt.node.config.network.iface(i)) for i
-                    in all_ifaces)
-
-    bridges = [nic for nic, info in all_infos.items()
-               if info["type"] == "bridge"]
-    vlans = [nic for nic, info in all_infos.items()
-             if info["type"] == "vlan"]
-    nics = [nic for nic, info in all_infos.items()
-            if info["name"] not in bridges + vlans]
-
-    LOGGER.debug("Bridges: %s" % bridges)
-    LOGGER.debug("VLANs: %s" % vlans)
-    LOGGER.debug("NICs: %s" % nics)
-
-    node_infos = {}
-    slaves = []
-    # Build dict with all NICs
-    for iface in nics:
-        LOGGER.debug("Adding physical NIC: %s" % iface)
-        info = all_infos[iface]
-        info.update(all_cfgs[iface])
-        node_infos[iface] = info
-        if info["bridge"]:
-            LOGGER.debug("Physical NIC '%s' is slave of '%s'" %
-                         (iface, info["bridge"]))
-            slaves.append(iface)
-
-    # Merge informations of VLANs into parent
-    for iface in vlans:
-        info = all_infos[iface]
-        info.update(all_cfgs[iface])
-        parent = info["vlan_parent"]
-        LOGGER.debug("Updating VLANID of '%s': %s" % (parent, info["vlanid"]))
-        node_infos[parent]["vlanid"] = info["vlanid"]
-        if info["bridge"]:
-            LOGGER.debug("VLAN NIC '%s' is slave of '%s'" %
-                         (iface, info["bridge"]))
-            slaves.append(iface)
-
-    for slave in slaves:
-        info = all_infos[slave]
-        info.update(all_cfgs[slave])
-        bridge = info["bridge"]
-        LOGGER.debug("Found slave for bridge '%s': %s" % (bridge, slave))
-        bridge_cfg = all_cfgs[bridge]
-        dst = slave
-        if info["is_vlan"]:
-            dst = info["vlan_parent"]
-        for k in ["bootproto", "ipaddr", "netmask", "gateway"]:
-            LOGGER.debug("Merging cfg %s from bridge %s into device %s" %
-                         (k, bridge, slave))
-            node_infos[dst][k] = bridge_cfg[k] if k in bridge_cfg else None
-
-    LOGGER.debug("Node NICs: %s" % node_infos)
-
-    return node_infos
+        return systype
 
 
 class NIC(base.Base):
-    def __init__(self, iface):
-        self.iface = iface
+    """Offers an API tp common NIC related functions is also a model for any
+    logical NIC.
+    """
+    ifname = None
+    vendor = None
+    driver = None
+    hwaddr = None
+    typ = None
+    config = None
+
+    def __init__(self, ifname):
         super(NIC, self).__init__()
+        self.ifname = ifname
+
+        self._udevinfo = UdevNICInfo(self.ifname)
+        self.vendor = self._udevinfo.vendor
+
+        self._sysfsinfo = SysfsNICInfo(self.ifname)
+        self.driver = self._sysfsinfo.driver
+        self.hwaddr = self._sysfsinfo.hwaddr
+
+        self.config = NicConfig(ifname)
+        self.typ = self._udevinfo.devtype or self._sysfsinfo.systype
 
     def exists(self):
         """If this NIC currently exists in the system
+
+        >>> NIC("lo").exists()
+        True
         """
-        return self.iface in all_ifaces()
+        return self.ifname in all_ifaces()
+
+    def is_configured(self):
+        """If there is a configuration for this NIC
+        """
+        return self.config.bootproto is not None
 
     def has_link(self):
         """Determin if L1 is up on a given interface
@@ -306,18 +207,18 @@
         True
 
         Args:
-            iface: The interface to be checked
+            ifname: The interface to be checked
         Returns:
             True if L1 (the-link-is-up) is detected (depends on driver support)
         """
 
         if not self.exists():
             raise UnknownNicError("Unknown network interface: '%s'" %
-                                  self.iface)
+                                  self.ifname)
 
-        if is_nm_managed(self.iface):
+        if is_nm_managed(self.ifname):
             try:
-                device = _nm_client.get_device_by_iface(self.iface)
+                device = _nm_client.get_device_by_iface(self.ifname)
                 if device:
                     return device.get_carrier()
             except:
@@ -326,11 +227,11 @@
         # Fallback
         has_carrier = False
         try:
-            with open("/sys/class/net/%s/carrier" % self.iface) as c:
+            with open("/sys/class/net/%s/carrier" % self.ifname) as c:
                 content = c.read()
                 has_carrier = "1" in content
         except:
-            LOGGER.debug("Carrier down for %s" % self.iface)
+            LOGGER.debug("Carrier down for %s" % self.ifname)
         return has_carrier
 
     def ipv4_address(self):
@@ -340,20 +241,20 @@
         return self.ip_addresses(["inet6"])["inet6"]
 
     def ip_addresses(self, families=["inet", "inet6"]):
-        """Get IP addresses for an iface
+        """Get IP addresses for an ifname
 
-        FIXME NM client.get_device_by_iface(iface).get_ip?_config()
+        FIXME NM _client.get_device_by_iface(ifname).get_ip?_config()
         """
         if not self.exists():
             raise UnknownNicError("Unknown network interface: '%s'" %
-                                  self.iface)
+                                  self.ifname)
 
         addresses = dict((f, (None, None)) for f in families)
 
         if False:
-            # FIXME to hackish to convert addr - is_nm_managed(iface):
-            device = _nm_client.get_device_by_iface(self.iface)
-            LOGGER.debug("Got '%s' for '%s'" % (device, self.iface))
+            # FIXME to hackish to convert addr - is_nm_managed(ifname):
+            device = _nm_client.get_device_by_iface(self.ifname)
+            LOGGER.debug("Got '%s' for '%s'" % (device, self.ifname))
             if device:
                 for family, cfgfunc, sf in [("inet", device.get_ip4_config,
                                              socket.AF_INET),
@@ -362,7 +263,7 @@
                     cfg = cfgfunc()
                     if not cfg:
                         LOGGER.debug("No %s configuration for %s" %
-                                     (family, self.iface))
+                                     (family, self.ifname))
                         break
                     addrs = cfg.get_addresses()
                     addr = addrs[0].get_address() if len(addrs) > 0 else None
@@ -371,7 +272,7 @@
             return addresses
 
         # Fallback
-        cmd = "ip -o addr show {iface}".format(iface=self.iface)
+        cmd = "ip -o addr show {ifname}".format(ifname=self.ifname)
         for line in process.pipe(cmd).split("\n"):
             token = re.split("\s+", line)
             if re.search("\sinet[6]?\s", line):
@@ -383,32 +284,233 @@
 
         return addresses
 
-    def vlanid(self):
-        vlanids = []
-        vcfg = "/proc/net/vlan/config"
-        pat = re.compile("([0-9]+)\s*\|\s*%s$" % self.iface)
-        if os.path.exists(vcfg):
-            try:
-                with open(vcfg) as f:
-                    for line in f:
-                        r = pat.search(line)
-                        if r:
-                            vlanids.append(r.groups[0])
-            except IOError as e:
-                self.logger.warning("Could not read vlan config: %s" %
-                                    e.message)
-        if len(vlanids) > 1:
-            self.logger.info("Found more than one (expected) vlan: %s" %
-                             vlanids)
-        return vlanids[0] if vlanids else None
+    def has_vlans(self):
+        """If this nic has associated vlan ids
+        """
+        return len(self.vlanids()) > 0
+
+    def is_vlan(self):
+        """if this nic is a vlan nic
+        """
+        vlans = Vlans()
+        return vlans.is_vlan_device(self.ifname)
+
+    def vlanids(self):
+        """Return all vlans of this nic
+        """
+        vlans = Vlans()
+        return vlans.vlans_for_nic(self.ifname)
 
     def identify(self):
         """Flash the lights of this NIC to identify it
         """
-        utils.process.call("ethtool --identify %s 10" % self.iface)
+        utils.process.call("ethtool --identify %s 10" % self.ifname)
 
     def __str__(self):
-        return "<NIC iface='%s' at %s" % (self.iface, hex(id(self)))
+        return self.build_str(["ifname"])
+
+    def __repr__(self):
+        return self.__str__()
+
+
+class BridgedNIC(NIC):
+    """A class to handle the legacy/default bridge-setup used by Node
+    """
+    bridge_nic = None
+    slave_nic = None
+
+    def __init__(self, snic, bnic):
+        super(BridgedNIC, self).__init__(snic.ifname)
+        self.slave_nic = snic
+        self.bridge_nic = bnic
+        self.config = self.bridge_nic.config
+
+    def exists(self):
+        return self.bridge_nic.exists()
+
+    def is_configured(self):
+        return self.bridge_nic.is_configured()
+
+    def has_link(self):
+        return self.slave_nic.has_link()
+
+    def ipv4_address(self):
+        return self.bridge_nic.ipv4_address()
+
+    def ipv6_address(self):
+        return self.bridge_nic.ipv6_address()
+
+    def ip_addresses(self, families=["inet", "inet6"]):
+        return self.bridge_nic.ip_addresses(families=families)
+
+    def is_vlan(self):
+        return self.slave_nic.is_vlan()
+
+    def has_vlans(self):
+        return self.slave_nic.has_vlans()
+
+    def vlanids(self):
+        return self.slave_nic.vlanids()
+
+    def identify(self):
+        self.slave_nic.identify(self)
+
+    def __str__(self):
+        pairs = {"bridge": self.bridge_nic.ifname,
+                 "slave": self.slave_nic.ifname}
+        return self.build_str(["ifname"], additional_pairs=pairs)
+
+
+class TaggedNIC(NIC):
+    """A class to provide easy access to tagged NICs
+    """
+    vlan_nic = None
+    parent_nic = None
+
+    def __init__(self, vnic):
+        parent_ifname, _ = TaggedNIC._parent_and_vlanid_from_name(vnic.ifname)
+        super(TaggedNIC, self).__init__(parent_ifname)
+        self.vlan_nic = vnic
+        self.parent_nic = NIC(parent_ifname)
+
+    @staticmethod
+    def _parent_and_vlanid_from_name(ifname):
+        """Parse parent and vlanid from a ifname
+
+        >>> TaggedNIC._parent_and_vlanid_from_name("ens1.0")
+        ('ens1', '0')
+
+        Args:
+            ifname
+        Returns:
+            A tuple (parent ifname, vlanid)
+        """
+        parts = ifname.split(".")
+        return (".".join(parts[:-1]), parts[-1:][0])
+
+    def exists(self):
+        return self.vlan_nic.exists()
+
+    def is_configured(self):
+        return self.vlan_nic.is_configured()
+
+    def has_link(self):
+        return self.parent_nic.has_link()
+
+    def ipv4_address(self):
+        return self.vlan_nic.ipv4_address()
+
+    def ipv6_address(self):
+        return self.vlan_nic.ipv6_address()
+
+    def ip_addresses(self, families=["inet", "inet6"]):
+        return self.vlan_nic.ip_addresses(families=families)
+
+    def is_vlan(self):
+        return True
+
+    def has_vlans(self):
+        raise RuntimeError("Nested tagging is not allowed. Is it?")
+
+    def vlanids(self):
+        return self.parent_nic.vlanids()
+
+    def identify(self):
+        self.parent_nic.identify(self)
+
+    def __str__(self):
+        pairs = {"vlan": self.vlan_nic.ifname,
+                 "parent": self.parent_nic.ifname}
+        return self.build_str(["ifname"], additional_pairs=pairs)
+
+
+class NodeNetwork(base.Base):
+    def all_ifnames(self):
+        return all_ifaces()
+
+    def relevant_ifnames(self, filter_bridges=True, filter_vlans=True):
+        all_ifaces = self.all_ifnames()
+        relevant_ifaces = self._filter_on_ifname(all_ifaces, filter_vlans)
+        irrelevant_names = set(all_ifaces) - set(relevant_ifaces)
+        LOGGER.debug("Irrelevant interfaces: %s" % irrelevant_names)
+        LOGGER.debug("Relevant interfaces: %s" % relevant_ifaces)
+
+        return relevant_ifaces
+
+    def _filter_on_ifname(self, ifnames, filter_vlans=True):
+        """Retuns relevant system NICs (via udev)
+
+        Filters out
+        - loop
+        - bonds
+        - vnets
+        - bridges
+        - sit
+        - vlans
+        - phy (wireless device)
+
+        >>> names = ["bond007", "sit1", "vnet11", "tun0", "wlan1", "virbr0"]
+        >>> names += ["ens1", "eth0", "phy0", "p1p7"]
+        >>> model = NodeNetwork()
+        >>> model._filter_on_ifname(names)
+        ['ens1', 'eth0', 'p1p7']
+
+        Args:
+            filter_bridges: If bridges shall be filtered out too
+            filter_vlans: If vlans shall be filtered out too
+        Returns:
+            List of strings, the NIC names
+        """
+        return [n for n in ifnames if not (n == "lo" or
+                                           n.startswith("bond") or
+                                           n.startswith("sit") or
+                                           n.startswith("vnet") or
+                                           n.startswith("tun") or
+                                           n.startswith("wlan") or
+                                           n.startswith("virbr") or
+                                           n.startswith("phy") or
+                                           (filter_vlans and ("." in n)))]
+
+    def build_nic_model(self, ifname):
+        nic = NIC(ifname)
+
+        if nic.config.bridge and nic.config.vlan:
+            nic = BridgedNIC(TaggedNIC(nic),
+                             NIC(nic.config.bridge))
+
+        elif nic.config.bridge:
+            nic = BridgedNIC(NIC(nic.ifname),
+                             NIC(nic.config.bridge))
+
+        elif nic.config.vlan:
+            nic = TaggedNIC(NIC(nic.ifname))
+
+        return nic
+
+    def nics(self):
+        """
+        >>> model = NodeNetwork()
+        """
+        nics = [NIC(ifname) for ifname
+                in self.relevant_ifnames(filter_vlans=False)]
+
+        bridges = [nic for nic in nics if nic.typ == "bridge"]
+        vlans = [nic for nic in nics if nic.typ == "vlan"]
+        nics = [nic for nic in nics if nic not in bridges + vlans]
+
+        self.logger.debug("Bridges: %s" % bridges)
+        self.logger.debug("VLANs: %s" % vlans)
+        self.logger.debug("NICs: %s" % nics)
+
+        candidates = {}
+
+        for nic in nics + vlans:
+            candidate = self.build_nic_model(nic.ifname)
+            if nic.ifname not in candidates or \
+               type(candidates[nic.ifname]) is NIC:
+                candidates[candidate.ifname] = candidate
+
+        return candidates
 
 
 class Routes(base.Base):
@@ -458,7 +560,8 @@
 def networking_status(iface=None):
     status = "Not connected"
 
-    iface = iface or config.network.node_bridge()
+    model = defaults.Network()
+    iface = iface or model.retrieve()["iface"]
     addresses = []
     if iface:
         nic = NIC(iface)
@@ -524,3 +627,52 @@
         print "Warning: could not find __res_init in libresolv.so.2"
         r = -1
     return r
+
+
+class Vlans(base.Base):
+    """A class to offer a convenience api to the vconfig file
+    """
+    cfg = "/proc/net/vlan/config"
+
+    def parse_cfg(self):
+        if not os.path.exists(self.cfg):
+            raise RuntimeError("vlans ain't enabled.")
+
+        vlans = {}
+        try:
+            with open(self.cfg) as f:
+                data_block = False
+                for line in f:
+                    if line.startswith("Name-Type"):
+                        data_block = True
+                        continue
+                    if not data_block:
+                        continue
+                    vdev, _, hdev = [field.strip()
+                                     for field in line.split("|")]
+                    if not hdev in vlans:
+                        vlans[hdev] = []
+                    vlans[hdev].append(vdev)
+        except IOError as e:
+            self.logger.warning("Could not read vlan config: %s" %
+                                e.message)
+
+        return vlans
+
+    def vlans_for_nic(self, ifname):
+        """return the vlans of the nic ifname
+        """
+        return self.parse_cfg().get(ifname, [])
+
+    def nic_for_vlan_device(self, vifname):
+        nic = None
+        for hdev, vifs in self.parse_cfg().items():
+            if vifname in vifs:
+                nic = hdev
+                break
+        return nic
+
+    def is_vlan_device(self, vifname):
+        """Check if ifname is a vlan device
+        """
+        return self.nic_for_vlan_device(vifname) is not None


--
To view, visit http://gerrit.ovirt.org/15411
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I8053ea21af5029190bc16b89ee109d2d7937a05f
Gerrit-PatchSet: 1
Gerrit-Project: ovirt-node
Gerrit-Branch: master
Gerrit-Owner: Fabian Deutsch <fabiand at fedoraproject.org>



More information about the node-patches mailing list