diff options
| author | Ben Howard <bh@digitalocean.com> | 2016-08-19 16:28:26 -0600 | 
|---|---|---|
| committer | Scott Moser <smoser@brickies.net> | 2016-09-29 09:57:13 -0400 | 
| commit | 9f83bb8e80806d3dd79ba426474dc3c696e19a41 (patch) | |
| tree | 90f695169bec78c544c9f2f345514360089231fd /cloudinit/sources/helpers | |
| parent | 6d1edc3f5a18b328bdd307426056539d5b9071fd (diff) | |
| download | vyos-cloud-init-9f83bb8e80806d3dd79ba426474dc3c696e19a41.tar.gz vyos-cloud-init-9f83bb8e80806d3dd79ba426474dc3c696e19a41.zip | |
DigitalOcean: use meta-data for network configruation
On DigitalOcean, Network information is provided via Meta-data.
It changes the datasource to be a local datasource, meaning it
will run before fallback networking is configured.
The advantage of that is that before networking is configured it
can bring up a network device with ipv4 link-local and hit the
metadata service that lives at 169.254.169.254 to find its networking
configuration.  It then takes down the link local address and lets
cloud-init configure networking.
The configuring of a network device to go looking for a metadata
service is gated by a check of data in the smbios.  This guarantees
that the code will not run on another system.
Diffstat (limited to 'cloudinit/sources/helpers')
| -rw-r--r-- | cloudinit/sources/helpers/digitalocean.py | 218 | 
1 files changed, 218 insertions, 0 deletions
| diff --git a/cloudinit/sources/helpers/digitalocean.py b/cloudinit/sources/helpers/digitalocean.py new file mode 100644 index 00000000..b0a721c2 --- /dev/null +++ b/cloudinit/sources/helpers/digitalocean.py @@ -0,0 +1,218 @@ +# vi: ts=4 expandtab +# +#    Author: Ben Howard  <bh@digitalocean.com> + +#    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 json +import random + +from cloudinit import log as logging +from cloudinit import net as cloudnet +from cloudinit import url_helper +from cloudinit import util + +NIC_MAP = {'public': 'eth0', 'private': 'eth1'} + +LOG = logging.getLogger(__name__) + + +def assign_ipv4_link_local(nic=None): +    """Bring up NIC using an address using link-local (ip4LL) IPs. On +       DigitalOcean, the link-local domain is per-droplet routed, so there +       is no risk of collisions. However, to be more safe, the ip4LL +       address is random. +    """ + +    if not nic: +        for cdev in sorted(cloudnet.get_devicelist()): +            if cloudnet.is_physical(cdev): +                nic = cdev +                LOG.debug("assigned nic '%s' for link-local discovery", nic) +                break + +    if not nic: +        raise RuntimeError("unable to find interfaces to access the" +                           "meta-data server. This droplet is broken.") + +    addr = "169.254.{0}.{1}/16".format(random.randint(1, 168), +                                       random.randint(0, 255)) + +    ip_addr_cmd = ['ip', 'addr', 'add', addr, 'dev', nic] +    ip_link_cmd = ['ip', 'link', 'set', 'dev', nic, 'up'] + +    if not util.which('ip'): +        raise RuntimeError("No 'ip' command available to configure ip4LL " +                           "address") + +    try: +        (result, _err) = util.subp(ip_addr_cmd) +        LOG.debug("assigned ip4LL address '%s' to '%s'", addr, nic) + +        (result, _err) = util.subp(ip_link_cmd) +        LOG.debug("brought device '%s' up", nic) +    except Exception: +        util.logexc(LOG, "ip4LL address assignment of '%s' to '%s' failed." +                         " Droplet networking will be broken", addr, nic) +        raise + +    return nic + + +def del_ipv4_link_local(nic=None): +    """Remove the ip4LL address. While this is not necessary, the ip4LL +       address is extraneous and confusing to users. +    """ +    if not nic: +        LOG.debug("no link_local address interface defined, skipping link " +                  "local address cleanup") +        return + +    LOG.debug("cleaning up ipv4LL address") + +    ip_addr_cmd = ['ip', 'addr', 'flush', 'dev', nic] + +    try: +        (result, _err) = util.subp(ip_addr_cmd) +        LOG.debug("removed ip4LL addresses from %s", nic) + +    except Exception as e: +        util.logexc(LOG, "failed to remove ip4LL address from '%s'.", nic, e) + + +def convert_network_configuration(config, dns_servers): +    """Convert the DigitalOcean Network description into Cloud-init's netconfig +       format. + +       Example JSON: +        {'public': [ +              {'mac': '04:01:58:27:7f:01', +               'ipv4': {'gateway': '45.55.32.1', +                        'netmask': '255.255.224.0', +                        'ip_address': '45.55.50.93'}, +               'anchor_ipv4': { +                        'gateway': '10.17.0.1', +                        'netmask': '255.255.0.0', +                        'ip_address': '10.17.0.9'}, +               'type': 'public', +               'ipv6': {'gateway': '....', +                        'ip_address': '....', +                        'cidr': 64}} +           ], +          'private': [ +              {'mac': '04:01:58:27:7f:02', +               'ipv4': {'gateway': '10.132.0.1', +                        'netmask': '255.255.0.0', +                        'ip_address': '10.132.75.35'}, +               'type': 'private'} +           ] +        } +    """ + +    def _get_subnet_part(pcfg, nameservers=None): +        subpart = {'type': 'static', +                   'control': 'auto', +                   'address': pcfg.get('ip_address'), +                   'gateway': pcfg.get('gateway')} + +        if nameservers: +            subpart['dns_nameservers'] = nameservers + +        if ":" in pcfg.get('ip_address'): +            subpart['address'] = "{0}/{1}".format(pcfg.get('ip_address'), +                                                  pcfg.get('cidr')) +        else: +            subpart['netmask'] = pcfg.get('netmask') + +        return subpart + +    all_nics = [] +    for k in ('public', 'private'): +        if k in config: +            all_nics.extend(config[k]) + +    macs_to_nics = cloudnet.get_interfaces_by_mac() +    nic_configs = [] + +    for nic in all_nics: + +        mac_address = nic.get('mac') +        sysfs_name = macs_to_nics.get(mac_address) +        nic_type = nic.get('type', 'unknown') +        # Note: the entry 'public' above contains a list, but +        # the list will only ever have one nic inside it per digital ocean. +        # If it ever had more than one nic, then this code would +        # assign all 'public' the same name. +        if_name = NIC_MAP.get(nic_type, sysfs_name) + +        LOG.debug("mapped %s interface to %s, assigning name of %s", +                  mac_address, sysfs_name, if_name) + +        ncfg = {'type': 'physical', +                'mac_address': mac_address, +                'name': if_name} + +        subnets = [] +        for netdef in ('ipv4', 'ipv6', 'anchor_ipv4', 'anchor_ipv6'): +            raw_subnet = nic.get(netdef, None) +            if not raw_subnet: +                continue + +            sub_part = _get_subnet_part(raw_subnet) +            if nic_type == 'public' and 'anchor' not in netdef: +                # add DNS resolvers to the public interfaces only +                sub_part = _get_subnet_part(raw_subnet, dns_servers) +            else: +                # remove the gateway any non-public interfaces +                if 'gateway' in sub_part: +                    del sub_part['gateway'] + +            subnets.append(sub_part) + +        ncfg['subnets'] = subnets +        nic_configs.append(ncfg) +        LOG.debug("nic '%s' configuration: %s", if_name, ncfg) + +    return {'version': 1, 'config': nic_configs} + + +def read_metadata(url, timeout=2, sec_between=2, retries=30): +    response = url_helper.readurl(url, timeout=timeout, +                                  sec_between=sec_between, retries=retries) +    if not response.ok(): +        raise RuntimeError("unable to read metadata at %s" % url) +    return json.loads(response.contents.decode()) + + +def read_sysinfo(): +    # DigitalOcean embeds vendor ID and instance/droplet_id in the +    # SMBIOS information + +    # Detect if we are on DigitalOcean and return the Droplet's ID +    vendor_name = util.read_dmi_data("system-manufacturer") +    if vendor_name != "DigitalOcean": +        return (False, None) + +    droplet_id = util.read_dmi_data("system-serial-number") +    if droplet_id: +        LOG.debug("system identified via SMBIOS as DigitalOcean Droplet: %s", +                  droplet_id) +    else: +        msg = ("system identified via SMBIOS as a DigitalOcean " +               "Droplet, but did not provide an ID. Please file a " +               "support ticket at: " +               "https://cloud.digitalocean.com/support/tickets/new") +        LOG.critical(msg) +        raise RuntimeError(msg) + +    return (True, droplet_id) | 
