summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Dymko <dymkod@gmail.com>2021-04-13 14:15:34 -0400
committerGitHub <noreply@github.com>2021-04-13 14:15:34 -0400
commit0ae0b1d4336acdcab12bd49e9bddb46922fb19c7 (patch)
tree11eca25e1c86dfeab2a80bcf027af9aebf2aec78
parent83f6bbfbe5b924be61a3c098f4202377d69c8947 (diff)
downloadcloud-init-git-0ae0b1d4336acdcab12bd49e9bddb46922fb19c7.tar.gz
Add Vultr support (#827)
This PR adds in support so that cloud-init can run on instances deployed on Vultr cloud. This was originally brought up in #628. Co-authored-by: Eric Benner <ebenner@vultr.com>
-rw-r--r--README.md2
-rw-r--r--cloudinit/apport.py1
-rw-r--r--cloudinit/settings.py1
-rw-r--r--cloudinit/sources/DataSourceVultr.py147
-rw-r--r--cloudinit/sources/helpers/vultr.py242
-rw-r--r--doc/rtd/topics/availability.rst1
-rw-r--r--doc/rtd/topics/datasources.rst2
-rw-r--r--doc/rtd/topics/datasources/vultr.rst35
-rw-r--r--doc/rtd/topics/network-config.rst5
-rw-r--r--tests/unittests/test_datasource/test_common.py2
-rw-r--r--tests/unittests/test_datasource/test_vultr.py343
-rw-r--r--tools/.github-cla-signers1
-rwxr-xr-xtools/ds-identify16
13 files changed, 795 insertions, 3 deletions
diff --git a/README.md b/README.md
index 435405da..aa6d84ae 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ get in contact with that distribution and send them our way!
| Supported OSes | Supported Public Clouds | Supported Private Clouds |
| --- | --- | --- |
-| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />|
+| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Vultr<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />|
## To start developing cloud-init
diff --git a/cloudinit/apport.py b/cloudinit/apport.py
index 25f254e3..aadc638f 100644
--- a/cloudinit/apport.py
+++ b/cloudinit/apport.py
@@ -41,6 +41,7 @@ KNOWN_CLOUD_NAMES = [
'SmartOS',
'UpCloud',
'VMware',
+ 'Vultr',
'ZStack',
'Other'
]
diff --git a/cloudinit/settings.py b/cloudinit/settings.py
index 91e1bfe7..23e4c0ad 100644
--- a/cloudinit/settings.py
+++ b/cloudinit/settings.py
@@ -30,6 +30,7 @@ CFG_BUILTIN = {
'GCE',
'OpenStack',
'AliYun',
+ 'Vultr',
'Ec2',
'CloudSigma',
'CloudStack',
diff --git a/cloudinit/sources/DataSourceVultr.py b/cloudinit/sources/DataSourceVultr.py
new file mode 100644
index 00000000..c08ff848
--- /dev/null
+++ b/cloudinit/sources/DataSourceVultr.py
@@ -0,0 +1,147 @@
+# Author: Eric Benner <ebenner@vultr.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+# Vultr Metadata API:
+# https://www.vultr.com/metadata/
+
+from cloudinit import log as log
+from cloudinit import sources
+from cloudinit import util
+
+import cloudinit.sources.helpers.vultr as vultr
+
+LOG = log.getLogger(__name__)
+BUILTIN_DS_CONFIG = {
+ 'url': 'http://169.254.169.254',
+ 'retries': 30,
+ 'timeout': 2,
+ 'wait': 2
+}
+
+
+class DataSourceVultr(sources.DataSource):
+
+ dsname = 'Vultr'
+
+ def __init__(self, sys_cfg, distro, paths):
+ super(DataSourceVultr, self).__init__(sys_cfg, distro, paths)
+ self.ds_cfg = util.mergemanydict([
+ util.get_cfg_by_path(sys_cfg, ["datasource", "Vultr"], {}),
+ BUILTIN_DS_CONFIG])
+
+ # Initiate data and check if Vultr
+ def _get_data(self):
+ LOG.debug("Detecting if machine is a Vultr instance")
+ if not vultr.is_vultr():
+ LOG.debug("Machine is not a Vultr instance")
+ return False
+
+ LOG.debug("Machine is a Vultr instance")
+
+ # Fetch metadata
+ md = self.get_metadata()
+
+ self.metadata_full = md
+ self.metadata['instanceid'] = md['instanceid']
+ self.metadata['local-hostname'] = md['hostname']
+ self.metadata['public-keys'] = md["public-keys"]
+ self.userdata_raw = md["user-data"]
+
+ # Generate config and process data
+ self.get_datasource_data(md)
+
+ # Dump some data so diagnosing failures is manageable
+ LOG.debug("Vultr Vendor Config:")
+ LOG.debug(md['vendor-data']['config'])
+ LOG.debug("SUBID: %s", self.metadata['instanceid'])
+ LOG.debug("Hostname: %s", self.metadata['local-hostname'])
+ if self.userdata_raw is not None:
+ LOG.debug("User-Data:")
+ LOG.debug(self.userdata_raw)
+
+ return True
+
+ # Process metadata
+ def get_datasource_data(self, md):
+ # Grab config
+ config = md['vendor-data']['config']
+
+ # Generate network config
+ self.netcfg = vultr.generate_network_config(md['interfaces'])
+
+ # This requires info generated in the vendor config
+ user_scripts = vultr.generate_user_scripts(md, self.netcfg['config'])
+
+ # Default hostname is "guest" for whitelabel
+ if self.metadata['local-hostname'] == "":
+ self.metadata['local-hostname'] = "guest"
+
+ self.userdata_raw = md["user-data"]
+ if self.userdata_raw == "":
+ self.userdata_raw = None
+
+ # Assemble vendor-data
+ # This adds provided scripts and the config
+ self.vendordata_raw = []
+ self.vendordata_raw.extend(user_scripts)
+ self.vendordata_raw.append("#cloud-config\n%s" % config)
+
+ # Get the metadata by flag
+ def get_metadata(self):
+ return vultr.get_metadata(self.ds_cfg['url'],
+ self.ds_cfg['timeout'],
+ self.ds_cfg['retries'],
+ self.ds_cfg['wait'])
+
+ # Compare subid as instance id
+ def check_instance_id(self, sys_cfg):
+ if not vultr.is_vultr():
+ return False
+
+ # Baremetal has no way to implement this in local
+ if vultr.is_baremetal():
+ return False
+
+ subid = vultr.get_sysinfo()['subid']
+ return sources.instance_id_matches_system_uuid(subid)
+
+ # Currently unsupported
+ @property
+ def launch_index(self):
+ return None
+
+ @property
+ def network_config(self):
+ return self.netcfg
+
+
+# Used to match classes to dependencies
+datasources = [
+ (DataSourceVultr, (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)
+
+
+if __name__ == "__main__":
+ import sys
+
+ if not vultr.is_vultr():
+ print("Machine is not a Vultr instance")
+ sys.exit(1)
+
+ md = vultr.get_metadata(BUILTIN_DS_CONFIG['url'],
+ BUILTIN_DS_CONFIG['timeout'],
+ BUILTIN_DS_CONFIG['retries'],
+ BUILTIN_DS_CONFIG['wait'])
+ config = md['vendor-data']['config']
+ sysinfo = vultr.get_sysinfo()
+
+ print(util.json_dumps(sysinfo))
+ print(config)
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/vultr.py b/cloudinit/sources/helpers/vultr.py
new file mode 100644
index 00000000..c22cd0b1
--- /dev/null
+++ b/cloudinit/sources/helpers/vultr.py
@@ -0,0 +1,242 @@
+# Author: Eric Benner <ebenner@vultr.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import json
+
+from cloudinit import log as log
+from cloudinit import url_helper
+from cloudinit import dmi
+from cloudinit import util
+from cloudinit import net
+from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError
+from functools import lru_cache
+
+# Get LOG
+LOG = log.getLogger(__name__)
+
+
+@lru_cache()
+def get_metadata(url, timeout, retries, sec_between):
+ # Bring up interface
+ try:
+ with EphemeralDHCPv4(connectivity_url=url):
+ # Fetch the metadata
+ v1 = read_metadata(url, timeout, retries, sec_between)
+ except (NoDHCPLeaseError) as exc:
+ LOG.error("Bailing, DHCP Exception: %s", exc)
+ raise
+
+ v1_json = json.loads(v1)
+ metadata = v1_json
+
+ return metadata
+
+
+# Read the system information from SMBIOS
+def get_sysinfo():
+ return {
+ 'manufacturer': dmi.read_dmi_data("system-manufacturer"),
+ 'subid': dmi.read_dmi_data("system-serial-number")
+ }
+
+
+# Assumes is Vultr is already checked
+def is_baremetal():
+ if get_sysinfo()['manufacturer'] != "Vultr":
+ return True
+ return False
+
+
+# Confirm is Vultr
+def is_vultr():
+ # VC2, VDC, and HFC use DMI
+ sysinfo = get_sysinfo()
+
+ if sysinfo['manufacturer'] == "Vultr":
+ return True
+
+ # Baremetal requires a kernel parameter
+ if "vultr" in util.get_cmdline().split():
+ return True
+
+ return False
+
+
+# Read Metadata endpoint
+def read_metadata(url, timeout, retries, sec_between):
+ url = "%s/v1.json" % url
+ response = url_helper.readurl(url,
+ timeout=timeout,
+ retries=retries,
+ headers={'Metadata-Token': 'vultr'},
+ sec_between=sec_between)
+
+ if not response.ok():
+ raise RuntimeError("Failed to connect to %s: Code: %s" %
+ url, response.code)
+
+ return response.contents.decode()
+
+
+# Wrapped for caching
+@lru_cache()
+def get_interface_map():
+ return net.get_interfaces_by_mac()
+
+
+# Convert macs to nics
+def get_interface_name(mac):
+ macs_to_nic = get_interface_map()
+
+ if mac not in macs_to_nic:
+ return None
+
+ return macs_to_nic.get(mac)
+
+
+# Generate network configs
+def generate_network_config(interfaces):
+ network = {
+ "version": 1,
+ "config": [
+ {
+ "type": "nameserver",
+ "address": [
+ "108.61.10.10"
+ ]
+ }
+ ]
+ }
+
+ # Prepare interface 0, public
+ if len(interfaces) > 0:
+ public = generate_public_network_interface(interfaces[0])
+ network['config'].append(public)
+
+ # Prepare interface 1, private
+ if len(interfaces) > 1:
+ private = generate_private_network_interface(interfaces[1])
+ network['config'].append(private)
+
+ return network
+
+
+# Input Metadata and generate public network config part
+def generate_public_network_interface(interface):
+ interface_name = get_interface_name(interface['mac'])
+ if not interface_name:
+ raise RuntimeError(
+ "Interface: %s could not be found on the system" %
+ interface['mac'])
+
+ netcfg = {
+ "name": interface_name,
+ "type": "physical",
+ "mac_address": interface['mac'],
+ "accept-ra": 1,
+ "subnets": [
+ {
+ "type": "dhcp",
+ "control": "auto"
+ },
+ {
+ "type": "dhcp6",
+ "control": "auto"
+ },
+ ]
+ }
+
+ # Check for additional IP's
+ additional_count = len(interface['ipv4']['additional'])
+ if "ipv4" in interface and additional_count > 0:
+ for additional in interface['ipv4']['additional']:
+ add = {
+ "type": "static",
+ "control": "auto",
+ "address": additional['address'],
+ "netmask": additional['netmask']
+ }
+ netcfg['subnets'].append(add)
+
+ # Check for additional IPv6's
+ additional_count = len(interface['ipv6']['additional'])
+ if "ipv6" in interface and additional_count > 0:
+ for additional in interface['ipv6']['additional']:
+ add = {
+ "type": "static6",
+ "control": "auto",
+ "address": additional['address'],
+ "netmask": additional['netmask']
+ }
+ netcfg['subnets'].append(add)
+
+ # Add config to template
+ return netcfg
+
+
+# Input Metadata and generate private network config part
+def generate_private_network_interface(interface):
+ interface_name = get_interface_name(interface['mac'])
+ if not interface_name:
+ raise RuntimeError(
+ "Interface: %s could not be found on the system" %
+ interface['mac'])
+
+ netcfg = {
+ "name": interface_name,
+ "type": "physical",
+ "mac_address": interface['mac'],
+ "accept-ra": 1,
+ "subnets": [
+ {
+ "type": "static",
+ "control": "auto",
+ "address": interface['ipv4']['address'],
+ "netmask": interface['ipv4']['netmask']
+ }
+ ]
+ }
+
+ return netcfg
+
+
+# This is for the vendor and startup scripts
+def generate_user_scripts(md, network_config):
+ user_scripts = []
+
+ # Raid 1 script
+ if md['vendor-data']['raid1-script']:
+ user_scripts.append(md['vendor-data']['raid1-script'])
+
+ # Enable multi-queue on linux
+ if util.is_Linux() and md['vendor-data']['ethtool-script']:
+ ethtool_script = md['vendor-data']['ethtool-script']
+
+ # Tool location
+ tool = "/opt/vultr/ethtool"
+
+ # Go through the interfaces
+ for netcfg in network_config:
+ # If the interface has a mac and is physical
+ if "mac_address" in netcfg and netcfg['type'] == "physical":
+ # Set its multi-queue to num of cores as per RHEL Docs
+ name = netcfg['name']
+ command = "%s -L %s combined $(nproc --all)" % (tool, name)
+ ethtool_script = '%s\n%s' % (ethtool_script, command)
+
+ user_scripts.append(ethtool_script)
+
+ # This is for vendor scripts
+ if md['vendor-data']['vendor-script']:
+ user_scripts.append(md['vendor-data']['vendor-script'])
+
+ # Startup script
+ script = md['startup-script']
+ if script and script != "echo No configured startup script":
+ user_scripts.append(script)
+
+ return user_scripts
+
+
+# vi: ts=4 expandtab
diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst
index f58b2b38..f3e13edc 100644
--- a/doc/rtd/topics/availability.rst
+++ b/doc/rtd/topics/availability.rst
@@ -56,6 +56,7 @@ environments in the public cloud:
- AltCloud
- SmartOS
- UpCloud
+- Vultr
Additionally, cloud-init is supported on these private clouds:
diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst
index 228173d2..497b1467 100644
--- a/doc/rtd/topics/datasources.rst
+++ b/doc/rtd/topics/datasources.rst
@@ -49,7 +49,7 @@ The following is a list of documents for each supported datasource:
datasources/smartos.rst
datasources/upcloud.rst
datasources/zstack.rst
-
+ datasources/vultr.rst
Creation
========
diff --git a/doc/rtd/topics/datasources/vultr.rst b/doc/rtd/topics/datasources/vultr.rst
new file mode 100644
index 00000000..e73406a8
--- /dev/null
+++ b/doc/rtd/topics/datasources/vultr.rst
@@ -0,0 +1,35 @@
+.. _datasource_vultr:
+
+Vultr
+=====
+
+The `Vultr`_ datasource retrieves basic configuration values from the locally
+accessible `metadata service`_. All data is served over HTTP from the address
+169.254.169.254. The endpoints are documented in,
+`https://www.vultr.com/metadata/
+<https://www.vultr.com/metadata/>`_
+
+Configuration
+-------------
+
+Vultr's datasource can be configured as follows:
+
+ datasource:
+ Vultr:
+ url: 'http://169.254.169.254'
+ retries: 3
+ timeout: 2
+ wait: 2
+
+- *url*: The URL used to aquire the metadata configuration from
+- *retries*: Determines the number of times to attempt to connect to the
+ metadata service
+- *timeout*: Determines the timeout in seconds to wait for a response from the
+ metadata service
+- *wait*: Determines the timeout in seconds to wait before retrying after
+ accessible failure
+
+.. _Vultr: https://www.vultr.com/
+.. _metadata service: https://www.vultr.com/metadata/
+
+.. vi: textwidth=78
diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst
index 07cad765..5f7a74f8 100644
--- a/doc/rtd/topics/network-config.rst
+++ b/doc/rtd/topics/network-config.rst
@@ -148,6 +148,10 @@ The following Datasources optionally provide network configuration:
- `UpCloud JSON metadata`_
+- :ref:`datasource_vultr`
+
+ - `Vultr JSON metadata`_
+
For more information on network configuration formats
.. toctree::
@@ -262,5 +266,6 @@ Example output converting V2 to sysconfig:
.. _OpenStack Metadata Service Network: https://specs.openstack.org/openstack/nova-specs/specs/liberty/implemented/metadata-service-network-info.html
.. _SmartOS JSON Metadata: https://eng.joyent.com/mdata/datadict.html
.. _UpCloud JSON metadata: https://developers.upcloud.com/1.3/8-servers/#metadata-service
+.. _Vultr JSON metadata: https://www.vultr.com/metadata/
.. vi: textwidth=78
diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py
index 5912f7ee..5e9c547a 100644
--- a/tests/unittests/test_datasource/test_common.py
+++ b/tests/unittests/test_datasource/test_common.py
@@ -28,6 +28,7 @@ from cloudinit.sources import (
DataSourceScaleway as Scaleway,
DataSourceSmartOS as SmartOS,
DataSourceUpCloud as UpCloud,
+ DataSourceVultr as Vultr,
)
from cloudinit.sources import DataSourceNone as DSNone
@@ -45,6 +46,7 @@ DEFAULT_LOCAL = [
Oracle.DataSourceOracle,
OVF.DataSourceOVF,
SmartOS.DataSourceSmartOS,
+ Vultr.DataSourceVultr,
Ec2.DataSourceEc2Local,
OpenStack.DataSourceOpenStackLocal,
RbxCloud.DataSourceRbxCloud,
diff --git a/tests/unittests/test_datasource/test_vultr.py b/tests/unittests/test_datasource/test_vultr.py
new file mode 100644
index 00000000..bbea2aa3
--- /dev/null
+++ b/tests/unittests/test_datasource/test_vultr.py
@@ -0,0 +1,343 @@
+# Author: Eric Benner <ebenner@vultr.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+# Vultr Metadata API:
+# https://www.vultr.com/metadata/
+
+import json
+
+from cloudinit import helpers
+from cloudinit import settings
+from cloudinit.sources import DataSourceVultr
+from cloudinit.sources.helpers import vultr
+
+from cloudinit.tests.helpers import mock, CiTestCase
+
+# Vultr metadata test data
+VULTR_V1_1 = {
+ 'bgp': {
+ 'ipv4': {
+ 'my-address': '',
+ 'my-asn': '',
+ 'peer-address': '',
+ 'peer-asn': ''
+ },
+ 'ipv6': {
+ 'my-address': '',
+ 'my-asn': '',
+ 'peer-address': '',
+ 'peer-asn': ''
+ }
+ },
+ 'hostname': 'CLOUDINIT_1',
+ 'instanceid': '42506325',
+ 'interfaces': [
+ {
+ 'ipv4': {
+ 'additional': [
+ ],
+ 'address': '108.61.89.242',
+ 'gateway': '108.61.89.1',
+ 'netmask': '255.255.255.0'
+ },
+ 'ipv6': {
+ 'additional': [
+ ],
+ 'address': '2001:19f0:5:56c2:5400:03ff:fe15:c465',
+ 'network': '2001:19f0:5:56c2::',
+ 'prefix': '64'
+ },
+ 'mac': '56:00:03:15:c4:65',
+ 'network-type': 'public'
+ }
+ ],
+ 'public-keys': [
+ 'ssh-rsa AAAAB3NzaC1y...IQQhv5PAOKaIl+mM3c= test3@key'
+ ],
+ 'region': {
+ 'regioncode': 'EWR'
+ },
+ 'user-defined': [
+ ],
+ 'startup-script': 'echo No configured startup script',
+ 'raid1-script': '',
+ 'user-data': [
+ ],
+ 'vendor-data': {
+ 'vendor-script': '',
+ 'ethtool-script': '',
+ 'config': {
+ 'package_upgrade': 'true',
+ 'disable_root': 0,
+ 'ssh_pwauth': 1,
+ 'chpasswd': {
+ 'expire': False,
+ 'list': [
+ 'root:$6$S2Smuj.../VqxmIR9Urw0jPZ88i4yvB/'
+ ]
+ },
+ 'system_info': {
+ 'default_user': {
+ 'name': 'root'
+ }
+ }
+ }
+ }
+}
+
+VULTR_V1_2 = {
+ 'bgp': {
+ 'ipv4': {
+ 'my-address': '',
+ 'my-asn': '',
+ 'peer-address': '',
+ 'peer-asn': ''
+ },
+ 'ipv6': {
+ 'my-address': '',
+ 'my-asn': '',
+ 'peer-address': '',
+ 'peer-asn': ''
+ }
+ },
+ 'hostname': 'CLOUDINIT_2',
+ 'instance-v2-id': '29bea708-2e6e-480a-90ad-0e6b5d5ad62f',
+ 'instanceid': '42872224',
+ 'interfaces': [
+ {
+ 'ipv4': {
+ 'additional': [
+ ],
+ 'address':'45.76.7.171',
+ 'gateway':'45.76.6.1',
+ 'netmask':'255.255.254.0'
+ },
+ 'ipv6':{
+ 'additional': [
+ ],
+ 'address':'2001:19f0:5:28a7:5400:03ff:fe1b:4eca',
+ 'network':'2001:19f0:5:28a7::',
+ 'prefix':'64'
+ },
+ 'mac':'56:00:03:1b:4e:ca',
+ 'network-type':'public'
+ },
+ {
+ 'ipv4': {
+ 'additional': [
+ ],
+ 'address':'10.1.112.3',
+ 'gateway':'',
+ 'netmask':'255.255.240.0'
+ },
+ 'ipv6':{
+ 'additional': [
+ ],
+ 'network':'',
+ 'prefix':''
+ },
+ 'mac':'5a:00:03:1b:4e:ca',
+ 'network-type':'private',
+ 'network-v2-id':'fbbe2b5b-b986-4396-87f5-7246660ccb64',
+ 'networkid':'net5e7155329d730'
+ }
+ ],
+ 'public-keys': [
+ 'ssh-rsa AAAAB3NzaC1y...IQQhv5PAOKaIl+mM3c= test3@key'
+ ],
+ 'region': {
+ 'regioncode': 'EWR'
+ },
+ 'user-defined': [
+ ],
+ 'startup-script': 'echo No configured startup script',
+ 'user-data': [
+ ],
+
+ 'vendor-data': {
+ 'vendor-script': '',
+ 'ethtool-script': '',
+ 'raid1-script': '',
+ 'config': {
+ 'package_upgrade': 'true',
+ 'disable_root': 0,
+ 'ssh_pwauth': 1,
+ 'chpasswd': {
+ 'expire': False,
+ 'list': [
+ 'root:$6$SxXx...k2mJNIzZB5vMCDBlYT1'
+ ]
+ },
+ 'system_info': {
+ 'default_user': {
+ 'name': 'root'
+ }
+ }
+ }
+ }
+}
+
+SSH_KEYS_1 = [
+ "ssh-rsa AAAAB3NzaC1y...IQQhv5PAOKaIl+mM3c= test3@key"
+]
+
+# Expected generated objects
+
+# Expected config
+EXPECTED_VULTR_CONFIG = {
+ 'package_upgrade': 'true',
+ 'disable_root': 0,
+ 'ssh_pwauth': 1,
+ 'chpasswd': {
+ 'expire': False,
+ 'list': [
+ 'root:$6$SxXx...k2mJNIzZB5vMCDBlYT1'
+ ]
+ },
+ 'system_info': {
+ 'default_user': {
+ 'name': 'root'
+ }
+ }
+}
+
+# Expected network config object from generator
+EXPECTED_VULTR_NETWORK_1 = {
+ 'version': 1,
+ 'config': [
+ {
+ 'type': 'nameserver',
+ 'address': ['108.61.10.10']
+ },
+ {
+ 'name': 'eth0',
+ 'type': 'physical',
+ 'mac_address': '56:00:03:15:c4:65',
+ 'accept-ra': 1,
+ 'subnets': [
+ {'type': 'dhcp', 'control': 'auto'},
+ {'type': 'dhcp6', 'control': 'auto'}
+ ],
+ }
+ ]
+}
+
+EXPECTED_VULTR_NETWORK_2 = {
+ 'version': 1,
+ 'config': [
+ {
+ 'type': 'nameserver',
+ 'address': ['108.61.10.10']
+ },
+ {
+ 'name': 'eth0',
+ 'type': 'physical',
+ 'mac_address': '56:00:03:1b:4e:ca',
+ 'accept-ra': 1,
+ 'subnets': [
+ {'type': 'dhcp', 'control': 'auto'},
+ {'type': 'dhcp6', 'control': 'auto'}
+ ],
+ },
+ {
+ 'name': 'eth1',
+ 'type': 'physical',
+ 'mac_address': '5a:00:03:1b:4e:ca',
+ 'accept-ra': 1,
+ 'subnets': [
+ {
+ "type": "static",
+ "control": "auto",
+ "address": "10.1.112.3",
+ "netmask": "255.255.240.0"
+ }
+ ],
+ }
+ ]
+}
+
+
+INTERFACE_MAP = {
+ '56:00:03:15:c4:65': 'eth0',
+ '56:00:03:1b:4e:ca': 'eth0',
+ '5a:00:03:1b:4e:ca': 'eth1'
+}
+
+
+class TestDataSourceVultr(CiTestCase):
+ def setUp(self):
+ super(TestDataSourceVultr, self).setUp()
+
+ # Stored as a dict to make it easier to maintain
+ raw1 = json.dumps(VULTR_V1_1['vendor-data']['config'])
+ raw2 = json.dumps(VULTR_V1_2['vendor-data']['config'])
+
+ # Make expected format
+ VULTR_V1_1['vendor-data']['config'] = raw1
+ VULTR_V1_2['vendor-data']['config'] = raw2
+
+ self.tmp = self.tmp_dir()
+
+ # Test the datasource itself
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ @mock.patch('cloudinit.sources.helpers.vultr.is_vultr')
+ @mock.patch('cloudinit.sources.helpers.vultr.get_metadata')
+ def test_datasource(self,
+ mock_getmeta,
+ mock_isvultr,
+ mock_netmap):
+ mock_getmeta.return_value = VULTR_V1_2
+ mock_isvultr.return_value = True
+ mock_netmap.return_value = INTERFACE_MAP
+
+ source = DataSourceVultr.DataSourceVultr(
+ settings.CFG_BUILTIN, None, helpers.Paths({'run_dir': self.tmp}))
+
+ # Test for failure
+ self.assertEqual(True, source._get_data())
+
+ # Test instance id
+ self.assertEqual("42872224", source.metadata['instanceid'])
+
+ # Test hostname
+ self.assertEqual("CLOUDINIT_2", source.metadata['local-hostname'])
+
+ # Test ssh keys
+ self.assertEqual(SSH_KEYS_1, source.metadata['public-keys'])
+
+ # Test vendor data generation
+ orig_val = self.maxDiff
+ self.maxDiff = None
+
+ vendordata = source.vendordata_raw
+
+ # Test vendor config
+ self.assertEqual(
+ EXPECTED_VULTR_CONFIG,
+ json.loads(vendordata[0].replace("#cloud-config", "")))
+
+ self.maxDiff = orig_val
+
+ # Test network config generation
+ self.assertEqual(EXPECTED_VULTR_NETWORK_2, source.network_config)
+
+ # Test network config generation
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ def test_network_config(self, mock_netmap):
+ mock_netmap.return_value = INTERFACE_MAP
+ interf = VULTR_V1_1['interfaces']
+
+ self.assertEqual(EXPECTED_VULTR_NETWORK_1,
+ vultr.generate_network_config(interf))
+
+ # Test Private Networking config generation
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ def test_private_network_config(self, mock_netmap):
+ mock_netmap.return_value = INTERFACE_MAP
+ interf = VULTR_V1_2['interfaces']
+
+ self.assertEqual(EXPECTED_VULTR_NETWORK_2,
+ vultr.generate_network_config(interf))
+
+# vi: ts=4 expandtab
diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers
index b39f4198..d6212d1d 100644
--- a/tools/.github-cla-signers
+++ b/tools/.github-cla-signers
@@ -11,6 +11,7 @@ BirknerAlex
candlerb
cawamata
dankenigsberg
+ddymko
dermotbradley
dhensby
eandersson
diff --git a/tools/ds-identify b/tools/ds-identify
index 2f2486f7..73e27c71 100755
--- a/tools/ds-identify
+++ b/tools/ds-identify
@@ -124,7 +124,7 @@ DI_DSNAME=""
# this has to match the builtin list in cloud-init, it is what will
# be searched if there is no setting found in config.
DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \
-CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \
+CloudSigma CloudStack DigitalOcean Vultr AliYun Ec2 GCE OpenNebula OpenStack \
OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud"
DI_DSLIST=""
DI_MODE=""
@@ -1350,6 +1350,20 @@ dscheck_IBMCloud() {
return ${DS_NOT_FOUND}
}
+dscheck_Vultr() {
+ dmi_sys_vendor_is Vultr && return $DS_FOUND
+
+ case " $DI_KERNEL_CMDLINE " in
+ *\ vultr\ *) return $DS_FOUND ;;
+ esac
+
+ if [ -f "${PATH_ROOT}/etc/vultr" ]; then
+ return $DS_FOUND
+ fi
+
+ return $DS_NOT_FOUND
+}
+
collect_info() {
read_uname_info
read_virt