# Author: Antti Myyrä <antti.myyra@upcloud.com>
#
# This file is part of cloud-init. See LICENSE file for license information.
import json
import logging
from cloudinit import dmi
from cloudinit import net as cloudnet
from cloudinit import url_helper
LOG = logging.getLogger(__name__)
def convert_to_network_config_v1(config):
"""
Convert the UpCloud network metadata description into
Cloud-init's version 1 netconfig format.
Example JSON:
{
"interfaces": [
{
"index": 1,
"ip_addresses": [
{
"address": "94.237.105.53",
"dhcp": true,
"dns": [
"94.237.127.9",
"94.237.40.9"
],
"family": "IPv4",
"floating": false,
"gateway": "94.237.104.1",
"network": "94.237.104.0/22"
},
{
"address": "94.237.105.50",
"dhcp": false,
"dns": [],
"family": "IPv4",
"floating": true,
"gateway": "",
"network": "94.237.105.50/32"
}
],
"mac": "32:d5:ba:4a:36:e7",
"network_id": "031457f4-0f8c-483c-96f2-eccede02909c",
"type": "public"
},
{
"index": 2,
"ip_addresses": [
{
"address": "10.6.3.27",
"dhcp": true,
"dns": [],
"family": "IPv4",
"floating": false,
"gateway": "10.6.0.1",
"network": "10.6.0.0/22"
}
],
"mac": "32:d5:ba:4a:84:cc",
"network_id": "03d82553-5bea-4132-b29a-e1cf67ec2dd1",
"type": "utility"
},
{
"index": 3,
"ip_addresses": [
{
"address": "2a04:3545:1000:720:38d6:baff:fe4a:63e7",
"dhcp": true,
"dns": [
"2a04:3540:53::1",
"2a04:3544:53::1"
],
"family": "IPv6",
"floating": false,
"gateway": "2a04:3545:1000:720::1",
"network": "2a04:3545:1000:720::/64"
}
],
"mac": "32:d5:ba:4a:63:e7",
"network_id": "03000000-0000-4000-8046-000000000000",
"type": "public"
},
{
"index": 4,
"ip_addresses": [
{
"address": "172.30.1.10",
"dhcp": true,
"dns": [],
"family": "IPv4",
"floating": false,
"gateway": "172.30.1.1",
"network": "172.30.1.0/24"
}
],
"mac": "32:d5:ba:4a:8a:e1",
"network_id": "035a0a4a-77b4-4de5-820d-189fc8135714",
"type": "private"
}
],
"dns": [
"94.237.127.9",
"94.237.40.9"
]
}
"""
def _get_subnet_config(ip_addr, dns):
if ip_addr.get("dhcp"):
dhcp_type = "dhcp"
if ip_addr.get("family") == "IPv6":
# UpCloud currently passes IPv6 addresses via
# StateLess Address Auto Configuration (SLAAC)
dhcp_type = "ipv6_dhcpv6-stateless"
return {"type": dhcp_type}
static_type = "static"
if ip_addr.get("family") == "IPv6":
static_type = "static6"
subpart = {
"type": static_type,
"control": "auto",
"address": ip_addr.get("address"),
}
if ip_addr.get("gateway"):
subpart["gateway"] = ip_addr.get("gateway")
if "/" in ip_addr.get("network"):
subpart["netmask"] = ip_addr.get("network").split("/")[1]
if dns != ip_addr.get("dns") and ip_addr.get("dns"):
subpart["dns_nameservers"] = ip_addr.get("dns")
return subpart
nic_configs = []
macs_to_interfaces = cloudnet.get_interfaces_by_mac()
LOG.debug("NIC mapping: %s", macs_to_interfaces)
for raw_iface in config.get("interfaces"):
LOG.debug("Considering %s", raw_iface)
mac_address = raw_iface.get("mac")
if mac_address not in macs_to_interfaces:
raise RuntimeError(
"Did not find network interface on system "
"with mac '%s'. Cannot apply configuration: %s"
% (mac_address, raw_iface)
)
iface_type = raw_iface.get("type")
sysfs_name = macs_to_interfaces.get(mac_address)
LOG.debug(
"Found %s interface '%s' with address '%s' (index %d)",
iface_type,
sysfs_name,
mac_address,
raw_iface.get("index"),
)
interface = {
"type": "physical",
"name": sysfs_name,
"mac_address": mac_address,
}
subnets = []
for ip_address in raw_iface.get("ip_addresses"):
sub_part = _get_subnet_config(ip_address, config.get("dns"))
subnets.append(sub_part)
interface["subnets"] = subnets
nic_configs.append(interface)
if config.get("dns"):
LOG.debug("Setting DNS nameservers to %s", config.get("dns"))
nic_configs.append(
{"type": "nameserver", "address": config.get("dns")}
)
return {"version": 1, "config": nic_configs}
def convert_network_config(config):
return convert_to_network_config_v1(config)
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():
# UpCloud embeds vendor ID and server UUID in the
# SMBIOS information
# Detect if we are on UpCloud and return the UUID
vendor_name = dmi.read_dmi_data("system-manufacturer")
if vendor_name != "UpCloud":
return False, None
server_uuid = dmi.read_dmi_data("system-uuid")
if server_uuid:
LOG.debug(
"system identified via SMBIOS as UpCloud server: %s", server_uuid
)
else:
msg = (
"system identified via SMBIOS as a UpCloud server, but "
"did not provide an ID. Please contact support via"
"https://hub.upcloud.com or via email with support@upcloud.com"
)
LOG.critical(msg)
raise RuntimeError(msg)
return True, server_uuid