summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2018-03-19 14:50:54 -0400
committerScott Moser <smoser@brickies.net>2018-03-19 14:50:54 -0400
commite88e35483e373b39b4485f30f7a867f50571027c (patch)
tree9acb68fc91dc94e2f6fd66d741ff6f75f4abe830 /cloudinit
parentde34dc7c467b318b2d04d065f8d752c7a530e155 (diff)
downloadvyos-cloud-init-e88e35483e373b39b4485f30f7a867f50571027c.tar.gz
vyos-cloud-init-e88e35483e373b39b4485f30f7a867f50571027c.zip
Add Hetzner Cloud DataSource
The Hetzner Cloud metadata service is an AWS-style service available over HTTP via the link local address 169.254.169.254. https://hetzner.com/cloud https://docs.hetzner.cloud/
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/apport.py6
-rw-r--r--cloudinit/settings.py1
-rw-r--r--cloudinit/sources/DataSourceHetzner.py100
-rw-r--r--cloudinit/sources/helpers/hetzner.py26
4 files changed, 130 insertions, 3 deletions
diff --git a/cloudinit/apport.py b/cloudinit/apport.py
index 221f341c..618b0160 100644
--- a/cloudinit/apport.py
+++ b/cloudinit/apport.py
@@ -14,9 +14,9 @@ except ImportError:
KNOWN_CLOUD_NAMES = [
'Amazon - Ec2', 'AliYun', 'AltCloud', 'Azure', 'Bigstep', 'CloudSigma',
- 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine', 'MAAS',
- 'NoCloud', 'OpenNebula', 'OpenStack', 'OVF', 'Scaleway', 'SmartOS',
- 'VMware', 'Other']
+ 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine',
+ 'Hetzner Cloud', 'MAAS', 'NoCloud', 'OpenNebula', 'OpenStack', 'OVF',
+ 'Scaleway', 'SmartOS', 'VMware', 'Other']
# Potentially clear text collected logs
CLOUDINIT_LOG = '/var/log/cloud-init.log'
diff --git a/cloudinit/settings.py b/cloudinit/settings.py
index c120498f..5fe749d4 100644
--- a/cloudinit/settings.py
+++ b/cloudinit/settings.py
@@ -36,6 +36,7 @@ CFG_BUILTIN = {
'SmartOS',
'Bigstep',
'Scaleway',
+ 'Hetzner',
# At the end to act as a 'catch' when none of the above work...
'None',
],
diff --git a/cloudinit/sources/DataSourceHetzner.py b/cloudinit/sources/DataSourceHetzner.py
new file mode 100644
index 00000000..769fe131
--- /dev/null
+++ b/cloudinit/sources/DataSourceHetzner.py
@@ -0,0 +1,100 @@
+# Author: Jonas Keidel <jonas.keidel@hetzner.com>
+# Author: Markus Schade <markus.schade@hetzner.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+#
+"""Hetzner Cloud API Documentation.
+ https://docs.hetzner.cloud/"""
+
+from cloudinit import log as logging
+from cloudinit import net as cloudnet
+from cloudinit import sources
+from cloudinit import util
+
+import cloudinit.sources.helpers.hetzner as hc_helper
+
+LOG = logging.getLogger(__name__)
+
+BASE_URL_V1 = 'http://169.254.169.254/hetzner/v1'
+
+BUILTIN_DS_CONFIG = {
+ 'metadata_url': BASE_URL_V1 + '/metadata',
+ 'userdata_url': BASE_URL_V1 + '/userdata',
+}
+
+MD_RETRIES = 60
+MD_TIMEOUT = 2
+MD_WAIT_RETRY = 2
+
+
+class DataSourceHetzner(sources.DataSource):
+ def __init__(self, sys_cfg, distro, paths):
+ sources.DataSource.__init__(self, sys_cfg, distro, paths)
+ self.distro = distro
+ self.metadata = dict()
+ self.ds_cfg = util.mergemanydict([
+ util.get_cfg_by_path(sys_cfg, ["datasource", "Hetzner"], {}),
+ BUILTIN_DS_CONFIG])
+ self.metadata_address = self.ds_cfg['metadata_url']
+ self.userdata_address = self.ds_cfg['userdata_url']
+ self.retries = self.ds_cfg.get('retries', MD_RETRIES)
+ self.timeout = self.ds_cfg.get('timeout', MD_TIMEOUT)
+ self.wait_retry = self.ds_cfg.get('wait_retry', MD_WAIT_RETRY)
+ self._network_config = None
+ self.dsmode = sources.DSMODE_NETWORK
+
+ def get_data(self):
+ nic = cloudnet.find_fallback_nic()
+ with cloudnet.EphemeralIPv4Network(nic, "169.254.0.1", 16,
+ "169.254.255.255"):
+ md = hc_helper.read_metadata(
+ self.metadata_address, timeout=self.timeout,
+ sec_between=self.wait_retry, retries=self.retries)
+ ud = hc_helper.read_userdata(
+ self.userdata_address, timeout=self.timeout,
+ sec_between=self.wait_retry, retries=self.retries)
+
+ self.userdata_raw = ud
+ self.metadata_full = md
+
+ """hostname is name provided by user at launch. The API enforces
+ it is a valid hostname, but it is not guaranteed to be resolvable
+ in dns or fully qualified."""
+ self.metadata['instance-id'] = md['instance-id']
+ self.metadata['local-hostname'] = md['hostname']
+ self.metadata['network-config'] = md.get('network-config', None)
+ self.metadata['public-keys'] = md.get('public-keys', None)
+ self.vendordata_raw = md.get("vendor_data", None)
+
+ return True
+
+ @property
+ def network_config(self):
+ """Configure the networking. This needs to be done each boot, since
+ the IP information may have changed due to snapshot and/or
+ migration.
+ """
+
+ if self._network_config:
+ return self._network_config
+
+ _net_config = self.metadata['network-config']
+ if not _net_config:
+ raise Exception("Unable to get meta-data from server....")
+
+ self._network_config = _net_config
+
+ return self._network_config
+
+
+# Used to match classes to dependencies
+datasources = [
+ (DataSourceHetzner, (sources.DEP_FILESYSTEM, )),
+]
+
+
+# Return a list of data sources that match this set of dependencies
+def get_datasource_list(depends):
+ return sources.list_from_depends(depends, datasources)
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/hetzner.py b/cloudinit/sources/helpers/hetzner.py
new file mode 100644
index 00000000..2554530d
--- /dev/null
+++ b/cloudinit/sources/helpers/hetzner.py
@@ -0,0 +1,26 @@
+# Author: Jonas Keidel <jonas.keidel@hetzner.com>
+# Author: Markus Schade <markus.schade@hetzner.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit import log as logging
+from cloudinit import url_helper
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+
+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 util.load_yaml(response.contents.decode())
+
+
+def read_userdata(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 userdata at %s" % url)
+ return response.contents