diff options
| author | Joshua Harlow <harlowja@gmail.com> | 2016-05-19 14:26:30 -0700 | 
|---|---|---|
| committer | Joshua Harlow <harlowja@gmail.com> | 2016-05-19 14:26:30 -0700 | 
| commit | 6b97c4e462b19374d6af807b1f5b9c087aa97996 (patch) | |
| tree | 3628a55f69b61644a519d5df40398d55ba55186c /cloudinit/net/eni.py | |
| parent | ea4bc2c603a9d964a918e01d00e39a851e979830 (diff) | |
| parent | 880d9fc2f9c62abf19b1506595aa81e5417dea45 (diff) | |
| download | vyos-cloud-init-6b97c4e462b19374d6af807b1f5b9c087aa97996.tar.gz vyos-cloud-init-6b97c4e462b19374d6af807b1f5b9c087aa97996.zip  | |
Remerge against head/master
Diffstat (limited to 'cloudinit/net/eni.py')
| -rw-r--r-- | cloudinit/net/eni.py | 396 | 
1 files changed, 396 insertions, 0 deletions
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py new file mode 100644 index 00000000..adb31c22 --- /dev/null +++ b/cloudinit/net/eni.py @@ -0,0 +1,396 @@ +# vi: ts=4 expandtab +# +#    This program is free software: you can redistribute it and/or modify +#    it under the terms of the GNU General Public License version 3, as +#    published by the Free Software Foundation. +# +#    This program is distributed in the hope that it will be useful, +#    but WITHOUT ANY WARRANTY; without even the implied warranty of +#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +#    GNU General Public License for more details. +# +#    You should have received a copy of the GNU General Public License +#    along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import glob +import os +import re + +from cloudinit import net + +from cloudinit.net import LINKS_FNAME_PREFIX +from cloudinit.net import ParserError +from cloudinit.net.udev import generate_udev_rule + + +NET_CONFIG_COMMANDS = [ +    "pre-up", "up", "post-up", "down", "pre-down", "post-down", +] + +NET_CONFIG_BRIDGE_OPTIONS = [ +    "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit", +    "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp", +] + +NET_CONFIG_OPTIONS = [ +    "address", "netmask", "broadcast", "network", "metric", "gateway", +    "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime", +    "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame", +    "netnum", "endpoint", "local", "ttl", +] + + +# TODO: switch valid_map based on mode inet/inet6 +def _iface_add_subnet(iface, subnet): +    content = "" +    valid_map = [ +        'address', +        'netmask', +        'broadcast', +        'metric', +        'gateway', +        'pointopoint', +        'mtu', +        'scope', +        'dns_search', +        'dns_nameservers', +    ] +    for key, value in subnet.items(): +        if value and key in valid_map: +            if type(value) == list: +                value = " ".join(value) +            if '_' in key: +                key = key.replace('_', '-') +            content += "    {} {}\n".format(key, value) + +    return content + + +# TODO: switch to valid_map for attrs +def _iface_add_attrs(iface): +    content = "" +    ignore_map = [ +        'control', +        'index', +        'inet', +        'mode', +        'name', +        'subnets', +        'type', +    ] +    if iface['type'] not in ['bond', 'bridge', 'vlan']: +        ignore_map.append('mac_address') + +    for key, value in iface.items(): +        if value and key not in ignore_map: +            if type(value) == list: +                value = " ".join(value) +            content += "    {} {}\n".format(key, value) + +    return content + + +def _iface_start_entry(iface, index): +    fullname = iface['name'] +    if index != 0: +        fullname += ":%s" % index + +    control = iface['control'] +    if control == "auto": +        cverb = "auto" +    elif control in ("hotplug",): +        cverb = "allow-" + control +    else: +        cverb = "# control-" + control + +    subst = iface.copy() +    subst.update({'fullname': fullname, 'cverb': cverb}) + +    return ("{cverb} {fullname}\n" +            "iface {fullname} {inet} {mode}\n").format(**subst) + + +def _parse_deb_config_data(ifaces, contents, src_dir, src_path): +    """Parses the file contents, placing result into ifaces. + +    '_source_path' is added to every dictionary entry to define which file +    the configration information came from. + +    :param ifaces: interface dictionary +    :param contents: contents of interfaces file +    :param src_dir: directory interfaces file was located +    :param src_path: file path the `contents` was read +    """ +    currif = None +    for line in contents.splitlines(): +        line = line.strip() +        if line.startswith('#'): +            continue +        split = line.split(' ') +        option = split[0] +        if option == "source-directory": +            parsed_src_dir = split[1] +            if not parsed_src_dir.startswith("/"): +                parsed_src_dir = os.path.join(src_dir, parsed_src_dir) +            for expanded_path in glob.glob(parsed_src_dir): +                dir_contents = os.listdir(expanded_path) +                dir_contents = [ +                    os.path.join(expanded_path, path) +                    for path in dir_contents +                    if (os.path.isfile(os.path.join(expanded_path, path)) and +                        re.match("^[a-zA-Z0-9_-]+$", path) is not None) +                ] +                for entry in dir_contents: +                    with open(entry, "r") as fp: +                        src_data = fp.read().strip() +                    abs_entry = os.path.abspath(entry) +                    _parse_deb_config_data( +                        ifaces, src_data, +                        os.path.dirname(abs_entry), abs_entry) +        elif option == "source": +            new_src_path = split[1] +            if not new_src_path.startswith("/"): +                new_src_path = os.path.join(src_dir, new_src_path) +            for expanded_path in glob.glob(new_src_path): +                with open(expanded_path, "r") as fp: +                    src_data = fp.read().strip() +                abs_path = os.path.abspath(expanded_path) +                _parse_deb_config_data( +                    ifaces, src_data, +                    os.path.dirname(abs_path), abs_path) +        elif option == "auto": +            for iface in split[1:]: +                if iface not in ifaces: +                    ifaces[iface] = { +                        # Include the source path this interface was found in. +                        "_source_path": src_path +                    } +                ifaces[iface]['auto'] = True +        elif option == "iface": +            iface, family, method = split[1:4] +            if iface not in ifaces: +                ifaces[iface] = { +                    # Include the source path this interface was found in. +                    "_source_path": src_path +                } +            elif 'family' in ifaces[iface]: +                raise ParserError( +                    "Interface %s can only be defined once. " +                    "Re-defined in '%s'." % (iface, src_path)) +            ifaces[iface]['family'] = family +            ifaces[iface]['method'] = method +            currif = iface +        elif option == "hwaddress": +            ifaces[currif]['hwaddress'] = split[1] +        elif option in NET_CONFIG_OPTIONS: +            ifaces[currif][option] = split[1] +        elif option in NET_CONFIG_COMMANDS: +            if option not in ifaces[currif]: +                ifaces[currif][option] = [] +            ifaces[currif][option].append(' '.join(split[1:])) +        elif option.startswith('dns-'): +            if 'dns' not in ifaces[currif]: +                ifaces[currif]['dns'] = {} +            if option == 'dns-search': +                ifaces[currif]['dns']['search'] = [] +                for domain in split[1:]: +                    ifaces[currif]['dns']['search'].append(domain) +            elif option == 'dns-nameservers': +                ifaces[currif]['dns']['nameservers'] = [] +                for server in split[1:]: +                    ifaces[currif]['dns']['nameservers'].append(server) +        elif option.startswith('bridge_'): +            if 'bridge' not in ifaces[currif]: +                ifaces[currif]['bridge'] = {} +            if option in NET_CONFIG_BRIDGE_OPTIONS: +                bridge_option = option.replace('bridge_', '', 1) +                ifaces[currif]['bridge'][bridge_option] = split[1] +            elif option == "bridge_ports": +                ifaces[currif]['bridge']['ports'] = [] +                for iface in split[1:]: +                    ifaces[currif]['bridge']['ports'].append(iface) +            elif option == "bridge_hw" and split[1].lower() == "mac": +                ifaces[currif]['bridge']['mac'] = split[2] +            elif option == "bridge_pathcost": +                if 'pathcost' not in ifaces[currif]['bridge']: +                    ifaces[currif]['bridge']['pathcost'] = {} +                ifaces[currif]['bridge']['pathcost'][split[1]] = split[2] +            elif option == "bridge_portprio": +                if 'portprio' not in ifaces[currif]['bridge']: +                    ifaces[currif]['bridge']['portprio'] = {} +                ifaces[currif]['bridge']['portprio'][split[1]] = split[2] +        elif option.startswith('bond-'): +            if 'bond' not in ifaces[currif]: +                ifaces[currif]['bond'] = {} +            bond_option = option.replace('bond-', '', 1) +            ifaces[currif]['bond'][bond_option] = split[1] +    for iface in ifaces.keys(): +        if 'auto' not in ifaces[iface]: +            ifaces[iface]['auto'] = False + + +def _parse_deb_config(path): +    """Parses a debian network configuration file.""" +    ifaces = {} +    with open(path, "r") as fp: +        contents = fp.read().strip() +    abs_path = os.path.abspath(path) +    _parse_deb_config_data( +        ifaces, contents, +        os.path.dirname(abs_path), abs_path) +    return ifaces + + +class Renderer(object): +    """Renders network information in a /etc/network/interfaces format.""" + +    def _render_persistent_net(self, network_state): +        """Given state, emit udev rules to map mac to ifname.""" +        content = "" +        interfaces = network_state.get('interfaces') +        for iface in interfaces.values(): +            # for physical interfaces write out a persist net udev rule +            if iface['type'] == 'physical' and \ +               'name' in iface and iface.get('mac_address'): +                content += generate_udev_rule(iface['name'], +                                              iface['mac_address']) + +        return content + +    def _render_route(self, route, indent=""): +        """ When rendering routes for an iface, in some cases applying a route +        may result in the route command returning non-zero which produces +        some confusing output for users manually using ifup/ifdown[1].  To +        that end, we will optionally include an '|| true' postfix to each +        route line allowing users to work with ifup/ifdown without using +        --force option. + +        We may at somepoint not want to emit this additional postfix, and +        add a 'strict' flag to this function.  When called with strict=True, +        then we will not append the postfix. + +        1. http://askubuntu.com/questions/168033/ +                 how-to-set-static-routes-in-ubuntu-server +        """ +        content = "" +        up = indent + "post-up route add" +        down = indent + "pre-down route del" +        eol = " || true\n" +        mapping = { +            'network': '-net', +            'netmask': 'netmask', +            'gateway': 'gw', +            'metric': 'metric', +        } +        if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0': +            default_gw = " default gw %s" % route['gateway'] +            content += up + default_gw + eol +            content += down + default_gw + eol +        elif route['network'] == '::' and route['netmask'] == 0: +            # ipv6! +            default_gw = " -A inet6 default gw %s" % route['gateway'] +            content += up + default_gw + eol +            content += down + default_gw + eol +        else: +            route_line = "" +            for k in ['network', 'netmask', 'gateway', 'metric']: +                if k in route: +                    route_line += " %s %s" % (mapping[k], route[k]) +            content += up + route_line + eol +            content += down + route_line + eol + +        return content + +    def _render_interfaces(self, network_state): +        ''' Given state, emit etc/network/interfaces content ''' + +        content = "" +        interfaces = network_state.get('interfaces') +        ''' Apply a sort order to ensure that we write out +            the physical interfaces first; this is critical for +            bonding +        ''' +        order = { +            'physical': 0, +            'bond': 1, +            'bridge': 2, +            'vlan': 3, +        } +        content += "auto lo\niface lo inet loopback\n" +        for dnskey, value in network_state.get('dns', {}).items(): +            if len(value): +                content += "    dns-{} {}\n".format(dnskey, " ".join(value)) + +        for iface in sorted(interfaces.values(), +                            key=lambda k: (order[k['type']], k['name'])): + +            if content[-2:] != "\n\n": +                content += "\n" +            subnets = iface.get('subnets', {}) +            if subnets: +                for index, subnet in zip(range(0, len(subnets)), subnets): +                    if content[-2:] != "\n\n": +                        content += "\n" +                    iface['index'] = index +                    iface['mode'] = subnet['type'] +                    iface['control'] = subnet.get('control', 'auto') +                    if iface['mode'].endswith('6'): +                        iface['inet'] += '6' +                    elif iface['mode'] == 'static' \ +                         and ":" in subnet['address']: +                        iface['inet'] += '6' +                    if iface['mode'].startswith('dhcp'): +                        iface['mode'] = 'dhcp' + +                    content += _iface_start_entry(iface, index) +                    content += _iface_add_subnet(iface, subnet) +                    content += _iface_add_attrs(iface) +            else: +                # ifenslave docs say to auto the slave devices +                if 'bond-master' in iface: +                    content += "auto {name}\n".format(**iface) +                content += "iface {name} {inet} {mode}\n".format(**iface) +                content += _iface_add_attrs(iface) + +        for route in network_state.get('routes'): +            content += self._render_route(route) + +        # global replacements until v2 format +        content = content.replace('mac_address', 'hwaddress') +        return content + +    def render_network_state(self, +        target, network_state, eni="etc/network/interfaces", +        links_prefix=LINKS_FNAME_PREFIX, +        netrules='etc/udev/rules.d/70-persistent-net.rules'): + +        fpeni = os.path.join(target, eni) +        net.write_file(fpeni, self._render_interfaces(network_state)) + +        if netrules: +            netrules = os.path.join(target, netrules) +            net.write_file(netrules, +                           self._render_persistent_net(network_state)) + +        if links_prefix: +            self._render_systemd_links(target, network_state, links_prefix) + +    def _render_systemd_links(self, target, network_state, +                              links_prefix=LINKS_FNAME_PREFIX): +        fp_prefix = os.path.sep.join((target, links_prefix)) +        for f in glob.glob(fp_prefix + "*"): +            os.unlink(f) +        interfaces = network_state.get('interfaces') +        for iface in interfaces.values(): +            if (iface['type'] == 'physical' and 'name' in iface and +                    iface.get('mac_address')): +                fname = fp_prefix + iface['name'] + ".link" +                content = "\n".join([ +                    "[Match]", +                    "MACAddress=" + iface['mac_address'], +                    "", +                    "[Link]", +                    "Name=" + iface['name'], +                    "" +                ]) +                net.write_file(fname, content)  | 
