summaryrefslogtreecommitdiff
path: root/cloudinit/sources
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/sources')
-rw-r--r--cloudinit/sources/DataSourceAliYun.py1
-rw-r--r--cloudinit/sources/DataSourceAltCloud.py7
-rw-r--r--cloudinit/sources/DataSourceAzure.py171
-rw-r--r--cloudinit/sources/DataSourceBigstep.py5
-rw-r--r--cloudinit/sources/DataSourceCloudSigma.py5
-rw-r--r--cloudinit/sources/DataSourceCloudStack.py5
-rw-r--r--cloudinit/sources/DataSourceConfigDrive.py9
-rw-r--r--cloudinit/sources/DataSourceDigitalOcean.py5
-rw-r--r--cloudinit/sources/DataSourceEc2.py65
-rw-r--r--cloudinit/sources/DataSourceGCE.py139
-rw-r--r--cloudinit/sources/DataSourceMAAS.py59
-rw-r--r--cloudinit/sources/DataSourceNoCloud.py5
-rw-r--r--cloudinit/sources/DataSourceNone.py5
-rw-r--r--cloudinit/sources/DataSourceOVF.py130
-rw-r--r--cloudinit/sources/DataSourceOpenNebula.py122
-rw-r--r--cloudinit/sources/DataSourceOpenStack.py5
-rw-r--r--cloudinit/sources/DataSourceScaleway.py4
-rw-r--r--cloudinit/sources/DataSourceSmartOS.py5
-rw-r--r--cloudinit/sources/__init__.py131
-rw-r--r--cloudinit/sources/helpers/azure.py25
-rw-r--r--cloudinit/sources/helpers/vmware/imc/config.py4
-rw-r--r--cloudinit/sources/helpers/vmware/imc/config_custom_script.py153
-rw-r--r--cloudinit/sources/helpers/vmware/imc/config_nic.py2
-rw-r--r--cloudinit/sources/tests/__init__.py0
-rw-r--r--cloudinit/sources/tests/test_init.py202
25 files changed, 1032 insertions, 232 deletions
diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py
index 43a7e42c..7ac8288d 100644
--- a/cloudinit/sources/DataSourceAliYun.py
+++ b/cloudinit/sources/DataSourceAliYun.py
@@ -11,6 +11,7 @@ ALIYUN_PRODUCT = "Alibaba Cloud ECS"
class DataSourceAliYun(EC2.DataSourceEc2):
+ dsname = 'AliYun'
metadata_urls = ['http://100.100.100.200']
# The minimum supported metadata_version from the ec2 metadata apis
diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py
index c78ad9eb..e1d0055b 100644
--- a/cloudinit/sources/DataSourceAltCloud.py
+++ b/cloudinit/sources/DataSourceAltCloud.py
@@ -74,6 +74,9 @@ def read_user_data_callback(mount_dir):
class DataSourceAltCloud(sources.DataSource):
+
+ dsname = 'AltCloud'
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.seed = None
@@ -112,7 +115,7 @@ class DataSourceAltCloud(sources.DataSource):
return 'UNKNOWN'
- def get_data(self):
+ def _get_data(self):
'''
Description:
User Data is passed to the launching instance which
@@ -142,7 +145,7 @@ class DataSourceAltCloud(sources.DataSource):
else:
cloud_type = self.get_cloud_type()
- LOG.debug('cloud_type: ' + str(cloud_type))
+ LOG.debug('cloud_type: %s', str(cloud_type))
if 'RHEV' in cloud_type:
if self.user_data_rhevm():
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 14367e9c..4bcbf3a4 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -11,13 +11,16 @@ from functools import partial
import os
import os.path
import re
+from time import time
from xml.dom import minidom
import xml.etree.ElementTree as ET
from cloudinit import log as logging
from cloudinit import net
+from cloudinit.net.dhcp import EphemeralDHCPv4
from cloudinit import sources
from cloudinit.sources.helpers.azure import get_metadata_from_fabric
+from cloudinit.url_helper import readurl, wait_for_url, UrlError
from cloudinit import util
LOG = logging.getLogger(__name__)
@@ -26,10 +29,16 @@ DS_NAME = 'Azure'
DEFAULT_METADATA = {"instance-id": "iid-AZURE-NODE"}
AGENT_START = ['service', 'walinuxagent', 'start']
AGENT_START_BUILTIN = "__builtin__"
-BOUNCE_COMMAND = [
+BOUNCE_COMMAND_IFUP = [
'sh', '-xc',
"i=$interface; x=0; ifdown $i || x=$?; ifup $i || x=$?; exit $x"
]
+BOUNCE_COMMAND_FREEBSD = [
+ 'sh', '-xc',
+ ("i=$interface; x=0; ifconfig down $i || x=$?; "
+ "ifconfig up $i || x=$?; exit $x")
+]
+
# azure systems will always have a resource disk, and 66-azure-ephemeral.rules
# ensures that it gets linked to this path.
RESOURCE_DISK_PATH = '/dev/disk/cloud/azure_resource'
@@ -38,6 +47,9 @@ LEASE_FILE = '/var/lib/dhcp/dhclient.eth0.leases'
DEFAULT_FS = 'ext4'
# DMI chassis-asset-tag is set static for all azure instances
AZURE_CHASSIS_ASSET_TAG = '7783-7084-3265-9085-8269-3286-77'
+REPROVISION_MARKER_FILE = "/var/lib/cloud/data/poll_imds"
+IMDS_URL = "http://169.254.169.254/metadata/reprovisiondata"
+IMDS_RETRIES = 5
def find_storvscid_from_sysctl_pnpinfo(sysctl_out, deviceid):
@@ -177,11 +189,6 @@ if util.is_FreeBSD():
RESOURCE_DISK_PATH = "/dev/" + res_disk
else:
LOG.debug("resource disk is None")
- BOUNCE_COMMAND = [
- 'sh', '-xc',
- ("i=$interface; x=0; ifconfig down $i || x=$?; "
- "ifconfig up $i || x=$?; exit $x")
- ]
BUILTIN_DS_CONFIG = {
'agent_command': AGENT_START_BUILTIN,
@@ -190,7 +197,7 @@ BUILTIN_DS_CONFIG = {
'hostname_bounce': {
'interface': DEFAULT_PRIMARY_NIC,
'policy': True,
- 'command': BOUNCE_COMMAND,
+ 'command': 'builtin',
'hostname_command': 'hostname',
},
'disk_aliases': {'ephemeral0': RESOURCE_DISK_PATH},
@@ -246,6 +253,8 @@ def temporary_hostname(temp_hostname, cfg, hostname_command='hostname'):
class DataSourceAzure(sources.DataSource):
+
+ dsname = 'Azure'
_negotiated = False
def __init__(self, sys_cfg, distro, paths):
@@ -273,19 +282,20 @@ class DataSourceAzure(sources.DataSource):
with temporary_hostname(azure_hostname, self.ds_cfg,
hostname_command=hostname_command) \
- as previous_hostname:
- if (previous_hostname is not None and
+ as previous_hn:
+ if (previous_hn is not None and
util.is_true(self.ds_cfg.get('set_hostname'))):
cfg = self.ds_cfg['hostname_bounce']
# "Bouncing" the network
try:
- perform_hostname_bounce(hostname=azure_hostname,
- cfg=cfg,
- prev_hostname=previous_hostname)
+ return perform_hostname_bounce(hostname=azure_hostname,
+ cfg=cfg,
+ prev_hostname=previous_hn)
except Exception as e:
LOG.warning("Failed publishing hostname: %s", e)
util.logexc(LOG, "handling set_hostname failed")
+ return False
def get_metadata_from_agent(self):
temp_hostname = self.metadata.get('local-hostname')
@@ -330,7 +340,7 @@ class DataSourceAzure(sources.DataSource):
metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files)
return metadata
- def get_data(self):
+ def _get_data(self):
# azure removes/ejects the cdrom containing the ovf-env.xml
# file on reboot. So, in order to successfully reboot we
# need to look in the datadir and consider that valid
@@ -342,15 +352,20 @@ class DataSourceAzure(sources.DataSource):
ddir = self.ds_cfg['data_dir']
candidates = [self.seed_dir]
+ if os.path.isfile(REPROVISION_MARKER_FILE):
+ candidates.insert(0, "IMDS")
candidates.extend(list_possible_azure_ds_devs())
if ddir:
candidates.append(ddir)
found = None
-
+ reprovision = False
for cdev in candidates:
try:
- if cdev.startswith("/dev/"):
+ if cdev == "IMDS":
+ ret = None
+ reprovision = True
+ elif cdev.startswith("/dev/"):
if util.is_FreeBSD():
ret = util.mount_cb(cdev, load_azure_ds_dir,
mtype="udf", sync=False)
@@ -367,6 +382,8 @@ class DataSourceAzure(sources.DataSource):
LOG.warning("%s was not mountable", cdev)
continue
+ if reprovision or self._should_reprovision(ret):
+ ret = self._reprovision()
(md, self.userdata_raw, cfg, files) = ret
self.seed = cdev
self.metadata = util.mergemanydict([md, DEFAULT_METADATA])
@@ -425,6 +442,83 @@ class DataSourceAzure(sources.DataSource):
LOG.debug("negotiating already done for %s",
self.get_instance_id())
+ def _poll_imds(self, report_ready=True):
+ """Poll IMDS for the new provisioning data until we get a valid
+ response. Then return the returned JSON object."""
+ url = IMDS_URL + "?api-version=2017-04-02"
+ headers = {"Metadata": "true"}
+ LOG.debug("Start polling IMDS")
+
+ def sleep_cb(response, loop_n):
+ return 1
+
+ def exception_cb(msg, exception):
+ if isinstance(exception, UrlError) and exception.code == 404:
+ return
+ LOG.warning("Exception during polling. Will try DHCP.",
+ exc_info=True)
+
+ # If we get an exception while trying to call IMDS, we
+ # call DHCP and setup the ephemeral network to acquire the new IP.
+ raise exception
+
+ need_report = report_ready
+ for i in range(IMDS_RETRIES):
+ try:
+ with EphemeralDHCPv4() as lease:
+ if need_report:
+ self._report_ready(lease=lease)
+ need_report = False
+ wait_for_url([url], max_wait=None, timeout=60,
+ status_cb=LOG.info,
+ headers_cb=lambda url: headers, sleep_time=1,
+ exception_cb=exception_cb,
+ sleep_time_cb=sleep_cb)
+ return str(readurl(url, headers=headers))
+ except Exception:
+ LOG.debug("Exception during polling-retrying dhcp" +
+ " %d more time(s).", (IMDS_RETRIES - i),
+ exc_info=True)
+
+ def _report_ready(self, lease):
+ """Tells the fabric provisioning has completed
+ before we go into our polling loop."""
+ try:
+ get_metadata_from_fabric(None, lease['unknown-245'])
+ except Exception as exc:
+ LOG.warning(
+ "Error communicating with Azure fabric; You may experience."
+ "connectivity issues.", exc_info=True)
+
+ def _should_reprovision(self, ret):
+ """Whether or not we should poll IMDS for reprovisioning data.
+ Also sets a marker file to poll IMDS.
+
+ The marker file is used for the following scenario: the VM boots into
+ this polling loop, which we expect to be proceeding infinitely until
+ the VM is picked. If for whatever reason the platform moves us to a
+ new host (for instance a hardware issue), we need to keep polling.
+ However, since the VM reports ready to the Fabric, we will not attach
+ the ISO, thus cloud-init needs to have a way of knowing that it should
+ jump back into the polling loop in order to retrieve the ovf_env."""
+ if not ret:
+ return False
+ (md, self.userdata_raw, cfg, files) = ret
+ path = REPROVISION_MARKER_FILE
+ if (cfg.get('PreprovisionedVm') is True or
+ os.path.isfile(path)):
+ if not os.path.isfile(path):
+ LOG.info("Creating a marker file to poll imds")
+ util.write_file(path, "%s: %s\n" % (os.getpid(), time()))
+ return True
+ return False
+
+ def _reprovision(self):
+ """Initiate the reprovisioning workflow."""
+ contents = self._poll_imds()
+ md, ud, cfg = read_azure_ovf(contents)
+ return (md, ud, cfg, {'ovf-env.xml': contents})
+
def _negotiate(self):
"""Negotiate with fabric and return data from it.
@@ -450,7 +544,7 @@ class DataSourceAzure(sources.DataSource):
"Error communicating with Azure fabric; You may experience."
"connectivity issues.", exc_info=True)
return False
-
+ util.del_file(REPROVISION_MARKER_FILE)
return fabric_data
def activate(self, cfg, is_new_instance):
@@ -580,18 +674,19 @@ def address_ephemeral_resize(devpath=RESOURCE_DISK_PATH, maxwait=120,
if os.path.exists(sempath):
try:
os.unlink(sempath)
- LOG.debug(bmsg + " removed.")
+ LOG.debug('%s removed.', bmsg)
except Exception as e:
# python3 throws FileNotFoundError, python2 throws OSError
- LOG.warning(bmsg + ": remove failed! (%s)", e)
+ LOG.warning('%s: remove failed! (%s)', bmsg, e)
else:
- LOG.debug(bmsg + " did not exist.")
+ LOG.debug('%s did not exist.', bmsg)
return
def perform_hostname_bounce(hostname, cfg, prev_hostname):
# set the hostname to 'hostname' if it is not already set to that.
# then, if policy is not off, bounce the interface using command
+ # Returns True if the network was bounced, False otherwise.
command = cfg['command']
interface = cfg['interface']
policy = cfg['policy']
@@ -604,8 +699,15 @@ def perform_hostname_bounce(hostname, cfg, prev_hostname):
env['old_hostname'] = prev_hostname
if command == "builtin":
- command = BOUNCE_COMMAND
-
+ if util.is_FreeBSD():
+ command = BOUNCE_COMMAND_FREEBSD
+ elif util.which('ifup'):
+ command = BOUNCE_COMMAND_IFUP
+ else:
+ LOG.debug(
+ "Skipping network bounce: ifupdown utils aren't present.")
+ # Don't bounce as networkd handles hostname DDNS updates
+ return False
LOG.debug("pubhname: publishing hostname [%s]", msg)
shell = not isinstance(command, (list, tuple))
# capture=False, see comments in bug 1202758 and bug 1206164.
@@ -613,6 +715,7 @@ def perform_hostname_bounce(hostname, cfg, prev_hostname):
get_uptime=True, func=util.subp,
kwargs={'args': command, 'shell': shell, 'capture': False,
'env': env})
+ return True
def crtfile_to_pubkey(fname, data=None):
@@ -829,9 +932,35 @@ def read_azure_ovf(contents):
if 'ssh_pwauth' not in cfg and password:
cfg['ssh_pwauth'] = True
+ cfg['PreprovisionedVm'] = _extract_preprovisioned_vm_setting(dom)
+
return (md, ud, cfg)
+def _extract_preprovisioned_vm_setting(dom):
+ """Read the preprovision flag from the ovf. It should not
+ exist unless true."""
+ platform_settings_section = find_child(
+ dom.documentElement,
+ lambda n: n.localName == "PlatformSettingsSection")
+ if not platform_settings_section or len(platform_settings_section) == 0:
+ LOG.debug("PlatformSettingsSection not found")
+ return False
+ platform_settings = find_child(
+ platform_settings_section[0],
+ lambda n: n.localName == "PlatformSettings")
+ if not platform_settings or len(platform_settings) == 0:
+ LOG.debug("PlatformSettings not found")
+ return False
+ preprovisionedVm = find_child(
+ platform_settings[0],
+ lambda n: n.localName == "PreprovisionedVm")
+ if not preprovisionedVm or len(preprovisionedVm) == 0:
+ LOG.debug("PreprovisionedVm not found")
+ return False
+ return util.translate_bool(preprovisionedVm[0].firstChild.nodeValue)
+
+
def encrypt_pass(password, salt_id="$6$"):
return crypt.crypt(password, salt_id + util.rand_str(strlen=16))
diff --git a/cloudinit/sources/DataSourceBigstep.py b/cloudinit/sources/DataSourceBigstep.py
index d7fcd45a..699a85b5 100644
--- a/cloudinit/sources/DataSourceBigstep.py
+++ b/cloudinit/sources/DataSourceBigstep.py
@@ -16,13 +16,16 @@ LOG = logging.getLogger(__name__)
class DataSourceBigstep(sources.DataSource):
+
+ dsname = 'Bigstep'
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.metadata = {}
self.vendordata_raw = ""
self.userdata_raw = ""
- def get_data(self, apply_filter=False):
+ def _get_data(self, apply_filter=False):
url = get_url_from_file()
if url is None:
return False
diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py
index 19df16b1..4eaad475 100644
--- a/cloudinit/sources/DataSourceCloudSigma.py
+++ b/cloudinit/sources/DataSourceCloudSigma.py
@@ -23,6 +23,9 @@ class DataSourceCloudSigma(sources.DataSource):
For more information about CloudSigma's Server Context:
http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html
"""
+
+ dsname = 'CloudSigma'
+
def __init__(self, sys_cfg, distro, paths):
self.cepko = Cepko()
self.ssh_public_key = ''
@@ -46,7 +49,7 @@ class DataSourceCloudSigma(sources.DataSource):
LOG.warning("failed to query dmi data for system product name")
return False
- def get_data(self):
+ def _get_data(self):
"""
Metadata is the whole server context and /meta/cloud-config is used
as userdata.
diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 9dc473fc..0df545fc 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -65,6 +65,9 @@ class CloudStackPasswordServerClient(object):
class DataSourceCloudStack(sources.DataSource):
+
+ dsname = 'CloudStack'
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.seed_dir = os.path.join(paths.seed_dir, 'cs')
@@ -117,7 +120,7 @@ class DataSourceCloudStack(sources.DataSource):
def get_config_obj(self):
return self.cfg
- def get_data(self):
+ def _get_data(self):
seed_ret = {}
if util.read_optional_seed(seed_ret, base=(self.seed_dir + "/")):
self.userdata_raw = seed_ret['user-data']
diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py
index ef374f3f..b8db6267 100644
--- a/cloudinit/sources/DataSourceConfigDrive.py
+++ b/cloudinit/sources/DataSourceConfigDrive.py
@@ -25,13 +25,16 @@ DEFAULT_METADATA = {
"instance-id": DEFAULT_IID,
}
FS_TYPES = ('vfat', 'iso9660')
-LABEL_TYPES = ('config-2',)
+LABEL_TYPES = ('config-2', 'CONFIG-2')
POSSIBLE_MOUNTS = ('sr', 'cd')
OPTICAL_DEVICES = tuple(('/dev/%s%s' % (z, i) for z in POSSIBLE_MOUNTS
for i in range(0, 2)))
class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
+
+ dsname = 'ConfigDrive'
+
def __init__(self, sys_cfg, distro, paths):
super(DataSourceConfigDrive, self).__init__(sys_cfg, distro, paths)
self.source = None
@@ -50,7 +53,7 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
mstr += "[source=%s]" % (self.source)
return mstr
- def get_data(self):
+ def _get_data(self):
found = None
md = {}
results = {}
@@ -221,7 +224,7 @@ def find_candidate_devs(probe_optical=True):
config drive v2:
Disk should be:
* either vfat or iso9660 formated
- * labeled with 'config-2'
+ * labeled with 'config-2' or 'CONFIG-2'
"""
# query optical drive to get it in blkid cache for 2.6 kernels
if probe_optical:
diff --git a/cloudinit/sources/DataSourceDigitalOcean.py b/cloudinit/sources/DataSourceDigitalOcean.py
index 5e7e66be..e0ef665e 100644
--- a/cloudinit/sources/DataSourceDigitalOcean.py
+++ b/cloudinit/sources/DataSourceDigitalOcean.py
@@ -27,6 +27,9 @@ MD_USE_IPV4LL = True
class DataSourceDigitalOcean(sources.DataSource):
+
+ dsname = 'DigitalOcean'
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.distro = distro
@@ -44,7 +47,7 @@ class DataSourceDigitalOcean(sources.DataSource):
def _get_sysinfo(self):
return do_helper.read_sysinfo()
- def get_data(self):
+ def _get_data(self):
(is_do, droplet_id) = self._get_sysinfo()
# only proceed if we know we are on DigitalOcean
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index 7bbbfb63..e14553b3 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -14,7 +14,7 @@ import time
from cloudinit import ec2_utils as ec2
from cloudinit import log as logging
from cloudinit import net
-from cloudinit.net import dhcp
+from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError
from cloudinit import sources
from cloudinit import url_helper as uhelp
from cloudinit import util
@@ -31,6 +31,7 @@ _unset = "_unset"
class Platforms(object):
+ # TODO Rename and move to cloudinit.cloud.CloudNames
ALIYUN = "AliYun"
AWS = "AWS"
BRIGHTBOX = "Brightbox"
@@ -45,6 +46,7 @@ class Platforms(object):
class DataSourceEc2(sources.DataSource):
+ dsname = 'Ec2'
# Default metadata urls that will be used if none are provided
# They will be checked for 'resolveability' and some of the
# following may be discarded if they do not resolve
@@ -68,11 +70,15 @@ class DataSourceEc2(sources.DataSource):
_fallback_interface = None
def __init__(self, sys_cfg, distro, paths):
- sources.DataSource.__init__(self, sys_cfg, distro, paths)
+ super(DataSourceEc2, self).__init__(sys_cfg, distro, paths)
self.metadata_address = None
self.seed_dir = os.path.join(paths.seed_dir, "ec2")
- def get_data(self):
+ def _get_cloud_name(self):
+ """Return the cloud name as identified during _get_data."""
+ return self.cloud_platform
+
+ def _get_data(self):
seed_ret = {}
if util.read_optional_seed(seed_ret, base=(self.seed_dir + "/")):
self.userdata_raw = seed_ret['user-data']
@@ -96,22 +102,13 @@ class DataSourceEc2(sources.DataSource):
if util.is_FreeBSD():
LOG.debug("FreeBSD doesn't support running dhclient with -sf")
return False
- dhcp_leases = dhcp.maybe_perform_dhcp_discovery(
- self.fallback_interface)
- if not dhcp_leases:
- # DataSourceEc2Local failed in init-local stage. DataSourceEc2
- # will still run in init-network stage.
+ try:
+ with EphemeralDHCPv4(self.fallback_interface):
+ return util.log_time(
+ logfunc=LOG.debug, msg='Crawl of metadata service',
+ func=self._crawl_metadata)
+ except NoDHCPLeaseError:
return False
- dhcp_opts = dhcp_leases[-1]
- net_params = {'interface': dhcp_opts.get('interface'),
- 'ip': dhcp_opts.get('fixed-address'),
- 'prefix_or_mask': dhcp_opts.get('subnet-mask'),
- 'broadcast': dhcp_opts.get('broadcast-address'),
- 'router': dhcp_opts.get('routers')}
- with net.EphemeralIPv4Network(**net_params):
- return util.log_time(
- logfunc=LOG.debug, msg='Crawl of metadata service',
- func=self._crawl_metadata)
else:
return self._crawl_metadata()
@@ -148,7 +145,12 @@ class DataSourceEc2(sources.DataSource):
return self.min_metadata_version
def get_instance_id(self):
- return self.metadata['instance-id']
+ if self.cloud_platform == Platforms.AWS:
+ # Prefer the ID from the instance identity document, but fall back
+ return self.identity.get(
+ 'instanceId', self.metadata['instance-id'])
+ else:
+ return self.metadata['instance-id']
def _get_url_settings(self):
mcfg = self.ds_cfg
@@ -262,19 +264,31 @@ class DataSourceEc2(sources.DataSource):
@property
def availability_zone(self):
try:
- return self.metadata['placement']['availability-zone']
+ if self.cloud_platform == Platforms.AWS:
+ return self.identity.get(
+ 'availabilityZone',
+ self.metadata['placement']['availability-zone'])
+ else:
+ return self.metadata['placement']['availability-zone']
except KeyError:
return None
@property
def region(self):
- az = self.availability_zone
- if az is not None:
- return az[:-1]
+ if self.cloud_platform == Platforms.AWS:
+ region = self.identity.get('region')
+ # Fallback to trimming the availability zone if region is missing
+ if self.availability_zone and not region:
+ region = self.availability_zone[:-1]
+ return region
+ else:
+ az = self.availability_zone
+ if az is not None:
+ return az[:-1]
return None
@property
- def cloud_platform(self):
+ def cloud_platform(self): # TODO rename cloud_name
if self._cloud_platform is None:
self._cloud_platform = identify_platform()
return self._cloud_platform
@@ -351,6 +365,9 @@ class DataSourceEc2(sources.DataSource):
api_version, self.metadata_address)
self.metadata = ec2.get_instance_metadata(
api_version, self.metadata_address)
+ if self.cloud_platform == Platforms.AWS:
+ self.identity = ec2.get_instance_identity(
+ api_version, self.metadata_address).get('document', {})
except Exception:
util.logexc(
LOG, "Failed reading from metadata address %s",
diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py
index ccae4200..2da34a99 100644
--- a/cloudinit/sources/DataSourceGCE.py
+++ b/cloudinit/sources/DataSourceGCE.py
@@ -2,8 +2,12 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+import datetime
+import json
+
from base64 import b64decode
+from cloudinit.distros import ug_util
from cloudinit import log as logging
from cloudinit import sources
from cloudinit import url_helper
@@ -17,16 +21,18 @@ REQUIRED_FIELDS = ('instance-id', 'availability-zone', 'local-hostname')
class GoogleMetadataFetcher(object):
- headers = {'X-Google-Metadata-Request': 'True'}
+ headers = {'Metadata-Flavor': 'Google'}
def __init__(self, metadata_address):
self.metadata_address = metadata_address
- def get_value(self, path, is_text):
+ def get_value(self, path, is_text, is_recursive=False):
value = None
try:
- resp = url_helper.readurl(url=self.metadata_address + path,
- headers=self.headers)
+ url = self.metadata_address + path
+ if is_recursive:
+ url += '/?recursive=True'
+ resp = url_helper.readurl(url=url, headers=self.headers)
except url_helper.UrlError as exc:
msg = "url %s raised exception %s"
LOG.debug(msg, path, exc)
@@ -35,22 +41,29 @@ class GoogleMetadataFetcher(object):
if is_text:
value = util.decode_binary(resp.contents)
else:
- value = resp.contents
+ value = resp.contents.decode('utf-8')
else:
LOG.debug("url %s returned code %s", path, resp.code)
return value
class DataSourceGCE(sources.DataSource):
+
+ dsname = 'GCE'
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
+ self.default_user = None
+ if distro:
+ (users, _groups) = ug_util.normalize_users_groups(sys_cfg, distro)
+ (self.default_user, _user_config) = ug_util.extract_default(users)
self.metadata = dict()
self.ds_cfg = util.mergemanydict([
util.get_cfg_by_path(sys_cfg, ["datasource", "GCE"], {}),
BUILTIN_DS_CONFIG])
self.metadata_address = self.ds_cfg['metadata_url']
- def get_data(self):
+ def _get_data(self):
ret = util.log_time(
LOG.debug, 'Crawl of GCE metadata service',
read_md, kwargs={'address': self.metadata_address})
@@ -67,17 +80,18 @@ class DataSourceGCE(sources.DataSource):
@property
def launch_index(self):
- # GCE does not provide lauch_index property
+ # GCE does not provide lauch_index property.
return None
def get_instance_id(self):
return self.metadata['instance-id']
def get_public_ssh_keys(self):
- return self.metadata['public-keys']
+ public_keys_data = self.metadata['public-keys-data']
+ return _parse_public_keys(public_keys_data, self.default_user)
def get_hostname(self, fqdn=False, resolve_ip=False):
- # GCE has long FDQN's and has asked for short hostnames
+ # GCE has long FDQN's and has asked for short hostnames.
return self.metadata['local-hostname'].split('.')[0]
@property
@@ -89,15 +103,58 @@ class DataSourceGCE(sources.DataSource):
return self.availability_zone.rsplit('-', 1)[0]
-def _trim_key(public_key):
- # GCE takes sshKeys attribute in the format of '<user>:<public_key>'
- # so we have to trim each key to remove the username part
+def _has_expired(public_key):
+ # Check whether an SSH key is expired. Public key input is a single SSH
+ # public key in the GCE specific key format documented here:
+ # https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys#sshkeyformat
+ try:
+ # Check for the Google-specific schema identifier.
+ schema, json_str = public_key.split(None, 3)[2:]
+ except (ValueError, AttributeError):
+ return False
+
+ # Do not expire keys if they do not have the expected schema identifier.
+ if schema != 'google-ssh':
+ return False
+
+ try:
+ json_obj = json.loads(json_str)
+ except ValueError:
+ return False
+
+ # Do not expire keys if there is no expriation timestamp.
+ if 'expireOn' not in json_obj:
+ return False
+
+ expire_str = json_obj['expireOn']
+ format_str = '%Y-%m-%dT%H:%M:%S+0000'
try:
- index = public_key.index(':')
- if index > 0:
- return public_key[(index + 1):]
- except Exception:
- return public_key
+ expire_time = datetime.datetime.strptime(expire_str, format_str)
+ except ValueError:
+ return False
+
+ # Expire the key if and only if we have exceeded the expiration timestamp.
+ return datetime.datetime.utcnow() > expire_time
+
+
+def _parse_public_keys(public_keys_data, default_user=None):
+ # Parse the SSH key data for the default user account. Public keys input is
+ # a list containing SSH public keys in the GCE specific key format
+ # documented here:
+ # https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys#sshkeyformat
+ public_keys = []
+ if not public_keys_data:
+ return public_keys
+ for public_key in public_keys_data:
+ if not public_key or not all(ord(c) < 128 for c in public_key):
+ continue
+ split_public_key = public_key.split(':', 1)
+ if len(split_public_key) != 2:
+ continue
+ user, key = split_public_key
+ if user in ('cloudinit', default_user) and not _has_expired(key):
+ public_keys.append(key)
+ return public_keys
def read_md(address=None, platform_check=True):
@@ -113,31 +170,28 @@ def read_md(address=None, platform_check=True):
ret['reason'] = "Not running on GCE."
return ret
- # if we cannot resolve the metadata server, then no point in trying
+ # If we cannot resolve the metadata server, then no point in trying.
if not util.is_resolvable_url(address):
LOG.debug("%s is not resolvable", address)
ret['reason'] = 'address "%s" is not resolvable' % address
return ret
- # url_map: (our-key, path, required, is_text)
+ # url_map: (our-key, path, required, is_text, is_recursive)
url_map = [
- ('instance-id', ('instance/id',), True, True),
- ('availability-zone', ('instance/zone',), True, True),
- ('local-hostname', ('instance/hostname',), True, True),
- ('public-keys', ('project/attributes/sshKeys',
- 'instance/attributes/ssh-keys'), False, True),
- ('user-data', ('instance/attributes/user-data',), False, False),
- ('user-data-encoding', ('instance/attributes/user-data-encoding',),
- False, True),
+ ('instance-id', ('instance/id',), True, True, False),
+ ('availability-zone', ('instance/zone',), True, True, False),
+ ('local-hostname', ('instance/hostname',), True, True, False),
+ ('instance-data', ('instance/attributes',), False, False, True),
+ ('project-data', ('project/attributes',), False, False, True),
]
metadata_fetcher = GoogleMetadataFetcher(address)
md = {}
- # iterate over url_map keys to get metadata items
- for (mkey, paths, required, is_text) in url_map:
+ # Iterate over url_map keys to get metadata items.
+ for (mkey, paths, required, is_text, is_recursive) in url_map:
value = None
for path in paths:
- new_value = metadata_fetcher.get_value(path, is_text)
+ new_value = metadata_fetcher.get_value(path, is_text, is_recursive)
if new_value is not None:
value = new_value
if required and value is None:
@@ -146,17 +200,23 @@ def read_md(address=None, platform_check=True):
return ret
md[mkey] = value
- if md['public-keys']:
- lines = md['public-keys'].splitlines()
- md['public-keys'] = [_trim_key(k) for k in lines]
+ instance_data = json.loads(md['instance-data'] or '{}')
+ project_data = json.loads(md['project-data'] or '{}')
+ valid_keys = [instance_data.get('sshKeys'), instance_data.get('ssh-keys')]
+ block_project = instance_data.get('block-project-ssh-keys', '').lower()
+ if block_project != 'true' and not instance_data.get('sshKeys'):
+ valid_keys.append(project_data.get('ssh-keys'))
+ valid_keys.append(project_data.get('sshKeys'))
+ public_keys_data = '\n'.join([key for key in valid_keys if key])
+ md['public-keys-data'] = public_keys_data.splitlines()
if md['availability-zone']:
md['availability-zone'] = md['availability-zone'].split('/')[-1]
- encoding = md.get('user-data-encoding')
+ encoding = instance_data.get('user-data-encoding')
if encoding:
if encoding == 'base64':
- md['user-data'] = b64decode(md['user-data'])
+ md['user-data'] = b64decode(instance_data.get('user-data'))
else:
LOG.warning('unknown user-data-encoding: %s, ignoring', encoding)
@@ -185,20 +245,19 @@ def platform_reports_gce():
return False
-# Used to match classes to dependencies
+# Used to match classes to dependencies.
datasources = [
(DataSourceGCE, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
]
-# Return a list of data sources that match this set of dependencies
+# 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 argparse
- import json
import sys
from base64 import b64encode
@@ -214,7 +273,7 @@ if __name__ == "__main__":
data = read_md(address=args.endpoint, platform_check=args.platform_check)
if 'user-data' in data:
# user-data is bytes not string like other things. Handle it specially.
- # if it can be represented as utf-8 then do so. Otherwise print base64
+ # If it can be represented as utf-8 then do so. Otherwise print base64
# encoded value in the key user-data-b64.
try:
data['user-data'] = data['user-data'].decode()
@@ -222,7 +281,7 @@ if __name__ == "__main__":
sys.stderr.write("User-data cannot be decoded. "
"Writing as base64\n")
del data['user-data']
- # b64encode returns a bytes value. decode to get the string.
+ # b64encode returns a bytes value. Decode to get the string.
data['user-data-b64'] = b64encode(data['user-data']).decode()
print(json.dumps(data, indent=1, sort_keys=True, separators=(',', ': ')))
diff --git a/cloudinit/sources/DataSourceMAAS.py b/cloudinit/sources/DataSourceMAAS.py
index 77df5a51..6ac88635 100644
--- a/cloudinit/sources/DataSourceMAAS.py
+++ b/cloudinit/sources/DataSourceMAAS.py
@@ -8,6 +8,7 @@
from __future__ import print_function
+import hashlib
import os
import time
@@ -39,30 +40,28 @@ class DataSourceMAAS(sources.DataSource):
hostname
vendor-data
"""
+
+ dsname = "MAAS"
+ id_hash = None
+ _oauth_helper = None
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.base_url = None
self.seed_dir = os.path.join(paths.seed_dir, 'maas')
- self.oauth_helper = self._get_helper()
-
- def _get_helper(self):
- mcfg = self.ds_cfg
- # If we are missing token_key, token_secret or consumer_key
- # then just do non-authed requests
- for required in ('token_key', 'token_secret', 'consumer_key'):
- if required not in mcfg:
- return url_helper.OauthUrlHelper()
+ self.id_hash = get_id_from_ds_cfg(self.ds_cfg)
- return url_helper.OauthUrlHelper(
- consumer_key=mcfg['consumer_key'], token_key=mcfg['token_key'],
- token_secret=mcfg['token_secret'],
- consumer_secret=mcfg.get('consumer_secret'))
+ @property
+ def oauth_helper(self):
+ if not self._oauth_helper:
+ self._oauth_helper = get_oauth_helper(self.ds_cfg)
+ return self._oauth_helper
def __str__(self):
root = sources.DataSource.__str__(self)
return "%s [%s]" % (root, self.base_url)
- def get_data(self):
+ def _get_data(self):
mcfg = self.ds_cfg
try:
@@ -144,6 +143,36 @@ class DataSourceMAAS(sources.DataSource):
return bool(url)
+ def check_instance_id(self, sys_cfg):
+ """locally check if the current system is the same instance.
+
+ MAAS doesn't provide a real instance-id, and if it did, it is
+ still only available over the network. We need to check based
+ only on local resources. So compute a hash based on Oauth tokens."""
+ if self.id_hash is None:
+ return False
+ ncfg = util.get_cfg_by_path(sys_cfg, ("datasource", self.dsname), {})
+ return (self.id_hash == get_id_from_ds_cfg(ncfg))
+
+
+def get_oauth_helper(cfg):
+ """Return an oauth helper instance for values in cfg.
+
+ @raises ValueError from OauthUrlHelper if some required fields have
+ true-ish values but others do not."""
+ keys = ('consumer_key', 'consumer_secret', 'token_key', 'token_secret')
+ kwargs = dict([(r, cfg.get(r)) for r in keys])
+ return url_helper.OauthUrlHelper(**kwargs)
+
+
+def get_id_from_ds_cfg(ds_cfg):
+ """Given a config, generate a unique identifier for this node."""
+ fields = ('consumer_key', 'token_key', 'token_secret')
+ idstr = '\0'.join([ds_cfg.get(f, "") for f in fields])
+ # store the encoding version as part of the hash in the event
+ # that it ever changed we can compute older versions.
+ return 'v1:' + hashlib.sha256(idstr.encode('utf-8')).hexdigest()
+
def read_maas_seed_dir(seed_d):
if seed_d.startswith("file://"):
@@ -319,7 +348,7 @@ if __name__ == "__main__":
sys.stderr.write("Must provide a url or a config with url.\n")
sys.exit(1)
- oauth_helper = url_helper.OauthUrlHelper(**creds)
+ oauth_helper = get_oauth_helper(creds)
def geturl(url):
# the retry is to ensure that oauth timestamp gets fixed
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index e641244d..5d3a8ddb 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -20,6 +20,9 @@ LOG = logging.getLogger(__name__)
class DataSourceNoCloud(sources.DataSource):
+
+ dsname = "NoCloud"
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.seed = None
@@ -32,7 +35,7 @@ class DataSourceNoCloud(sources.DataSource):
root = sources.DataSource.__str__(self)
return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode)
- def get_data(self):
+ def _get_data(self):
defaults = {
"instance-id": "nocloud",
"dsmode": self.dsmode,
diff --git a/cloudinit/sources/DataSourceNone.py b/cloudinit/sources/DataSourceNone.py
index 906bb278..e63a7e39 100644
--- a/cloudinit/sources/DataSourceNone.py
+++ b/cloudinit/sources/DataSourceNone.py
@@ -11,12 +11,15 @@ LOG = logging.getLogger(__name__)
class DataSourceNone(sources.DataSource):
+
+ dsname = "None"
+
def __init__(self, sys_cfg, distro, paths, ud_proc=None):
sources.DataSource.__init__(self, sys_cfg, distro, paths, ud_proc)
self.metadata = {}
self.userdata_raw = ''
- def get_data(self):
+ def _get_data(self):
# If the datasource config has any provided 'fallback'
# userdata or metadata, use it...
if 'userdata_raw' in self.ds_cfg:
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
index ccebf11a..6e62f984 100644
--- a/cloudinit/sources/DataSourceOVF.py
+++ b/cloudinit/sources/DataSourceOVF.py
@@ -21,6 +21,8 @@ from cloudinit import util
from cloudinit.sources.helpers.vmware.imc.config \
import Config
+from cloudinit.sources.helpers.vmware.imc.config_custom_script \
+ import PreCustomScript, PostCustomScript
from cloudinit.sources.helpers.vmware.imc.config_file \
import ConfigFile
from cloudinit.sources.helpers.vmware.imc.config_nic \
@@ -30,7 +32,7 @@ from cloudinit.sources.helpers.vmware.imc.config_passwd \
from cloudinit.sources.helpers.vmware.imc.guestcust_error \
import GuestCustErrorEnum
from cloudinit.sources.helpers.vmware.imc.guestcust_event \
- import GuestCustEventEnum
+ import GuestCustEventEnum as GuestCustEvent
from cloudinit.sources.helpers.vmware.imc.guestcust_state \
import GuestCustStateEnum
from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
@@ -43,6 +45,9 @@ LOG = logging.getLogger(__name__)
class DataSourceOVF(sources.DataSource):
+
+ dsname = "OVF"
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.seed = None
@@ -60,7 +65,7 @@ class DataSourceOVF(sources.DataSource):
root = sources.DataSource.__str__(self)
return "%s [seed=%s]" % (root, self.seed)
- def get_data(self):
+ def _get_data(self):
found = []
md = {}
ud = ""
@@ -124,17 +129,31 @@ class DataSourceOVF(sources.DataSource):
self._vmware_cust_conf = Config(cf)
(md, ud, cfg) = read_vmware_imc(self._vmware_cust_conf)
self._vmware_nics_to_enable = get_nics_to_enable(nicspath)
- markerid = self._vmware_cust_conf.marker_id
- markerexists = check_marker_exists(markerid)
+ imcdirpath = os.path.dirname(vmwareImcConfigFilePath)
+ product_marker = self._vmware_cust_conf.marker_id
+ hasmarkerfile = check_marker_exists(
+ product_marker, os.path.join(self.paths.cloud_dir, 'data'))
+ special_customization = product_marker and not hasmarkerfile
+ customscript = self._vmware_cust_conf.custom_script_name
except Exception as e:
- LOG.debug("Error parsing the customization Config File")
- LOG.exception(e)
- set_customization_status(
- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
- GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
- raise e
- finally:
- util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
+ _raise_error_status(
+ "Error parsing the customization Config File",
+ e,
+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
+ vmwareImcConfigFilePath)
+
+ if special_customization:
+ if customscript:
+ try:
+ precust = PreCustomScript(customscript, imcdirpath)
+ precust.execute()
+ except Exception as e:
+ _raise_error_status(
+ "Error executing pre-customization script",
+ e,
+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
+ vmwareImcConfigFilePath)
+
try:
LOG.debug("Preparing the Network configuration")
self._network_config = get_network_config_from_conf(
@@ -143,13 +162,13 @@ class DataSourceOVF(sources.DataSource):
True,
self.distro.osfamily)
except Exception as e:
- LOG.exception(e)
- set_customization_status(
- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
- GuestCustEventEnum.GUESTCUST_EVENT_NETWORK_SETUP_FAILED)
- raise e
+ _raise_error_status(
+ "Error preparing Network Configuration",
+ e,
+ GuestCustEvent.GUESTCUST_EVENT_NETWORK_SETUP_FAILED,
+ vmwareImcConfigFilePath)
- if markerid and not markerexists:
+ if special_customization:
LOG.debug("Applying password customization")
pwdConfigurator = PasswordConfigurator()
adminpwd = self._vmware_cust_conf.admin_password
@@ -161,27 +180,41 @@ class DataSourceOVF(sources.DataSource):
else:
LOG.debug("Changing password is not needed")
except Exception as e:
- LOG.debug("Error applying Password Configuration: %s", e)
- set_customization_status(
- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
- GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
- return False
- if markerid:
- LOG.debug("Handle marker creation")
+ _raise_error_status(
+ "Error applying Password Configuration",
+ e,
+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
+ vmwareImcConfigFilePath)
+
+ if customscript:
+ try:
+ postcust = PostCustomScript(customscript, imcdirpath)
+ postcust.execute()
+ except Exception as e:
+ _raise_error_status(
+ "Error executing post-customization script",
+ e,
+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
+ vmwareImcConfigFilePath)
+
+ if product_marker:
try:
- setup_marker_files(markerid)
+ setup_marker_files(
+ product_marker,
+ os.path.join(self.paths.cloud_dir, 'data'))
except Exception as e:
- LOG.debug("Error creating marker files: %s", e)
- set_customization_status(
- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
- GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
- return False
+ _raise_error_status(
+ "Error creating marker files",
+ e,
+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
+ vmwareImcConfigFilePath)
self._vmware_cust_found = True
found.append('vmware-tools')
# TODO: Need to set the status to DONE only when the
# customization is done successfully.
+ util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
enable_nics(self._vmware_nics_to_enable)
set_customization_status(
GuestCustStateEnum.GUESTCUST_STATE_DONE,
@@ -536,31 +569,52 @@ def get_datasource_list(depends):
# To check if marker file exists
-def check_marker_exists(markerid):
+def check_marker_exists(markerid, marker_dir):
"""
Check the existence of a marker file.
Presence of marker file determines whether a certain code path is to be
executed. It is needed for partial guest customization in VMware.
+ @param markerid: is an unique string representing a particular product
+ marker.
+ @param: marker_dir: The directory in which markers exist.
"""
if not markerid:
return False
- markerfile = "/.markerfile-" + markerid
+ markerfile = os.path.join(marker_dir, ".markerfile-" + markerid + ".txt")
if os.path.exists(markerfile):
return True
return False
# Create a marker file
-def setup_marker_files(markerid):
+def setup_marker_files(markerid, marker_dir):
"""
Create a new marker file.
Marker files are unique to a full customization workflow in VMware
environment.
+ @param markerid: is an unique string representing a particular product
+ marker.
+ @param: marker_dir: The directory in which markers exist.
+
"""
- if not markerid:
- return
- markerfile = "/.markerfile-" + markerid
- util.del_file("/.markerfile-*.txt")
+ LOG.debug("Handle marker creation")
+ markerfile = os.path.join(marker_dir, ".markerfile-" + markerid + ".txt")
+ for fname in os.listdir(marker_dir):
+ if fname.startswith(".markerfile"):
+ util.del_file(os.path.join(marker_dir, fname))
open(markerfile, 'w').close()
+
+def _raise_error_status(prefix, error, event, config_file):
+ """
+ Raise error and send customization status to the underlying VMware
+ Virtualization Platform. Also, cleanup the imc directory.
+ """
+ LOG.debug('%s: %s', prefix, error)
+ set_customization_status(
+ GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
+ event)
+ util.del_dir(os.path.dirname(config_file))
+ raise error
+
# vi: ts=4 expandtab
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
index 5fdac192..ce47b6bd 100644
--- a/cloudinit/sources/DataSourceOpenNebula.py
+++ b/cloudinit/sources/DataSourceOpenNebula.py
@@ -12,6 +12,7 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
+import collections
import os
import pwd
import re
@@ -19,6 +20,7 @@ import string
from cloudinit import log as logging
from cloudinit import net
+from cloudinit.net import eni
from cloudinit import sources
from cloudinit import util
@@ -31,6 +33,9 @@ CONTEXT_DISK_FILES = ["context.sh"]
class DataSourceOpenNebula(sources.DataSource):
+
+ dsname = "OpenNebula"
+
def __init__(self, sys_cfg, distro, paths):
sources.DataSource.__init__(self, sys_cfg, distro, paths)
self.seed = None
@@ -40,7 +45,7 @@ class DataSourceOpenNebula(sources.DataSource):
root = sources.DataSource.__str__(self)
return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode)
- def get_data(self):
+ def _get_data(self):
defaults = {"instance-id": DEFAULT_IID}
results = None
seed = None
@@ -86,11 +91,18 @@ class DataSourceOpenNebula(sources.DataSource):
return False
self.seed = seed
- self.network_eni = results.get("network_config")
+ self.network_eni = results.get('network-interfaces')
self.metadata = md
self.userdata_raw = results.get('userdata')
return True
+ @property
+ def network_config(self):
+ if self.network_eni is not None:
+ return eni.convert_eni_data(self.network_eni)
+ else:
+ return None
+
def get_hostname(self, fqdn=False, resolve_ip=None):
if resolve_ip is None:
if self.dsmode == sources.DSMODE_NETWORK:
@@ -113,58 +125,53 @@ class OpenNebulaNetwork(object):
self.context = context
if system_nics_by_mac is None:
system_nics_by_mac = get_physical_nics_by_mac()
- self.ifaces = system_nics_by_mac
+ self.ifaces = collections.OrderedDict(
+ [k for k in sorted(system_nics_by_mac.items(),
+ key=lambda k: net.natural_sort_key(k[1]))])
+
+ # OpenNebula 4.14+ provide macaddr for ETHX in variable ETH_MAC.
+ # context_devname provides {mac.lower():ETHX, mac2.lower():ETHX}
+ self.context_devname = {}
+ for k, v in context.items():
+ m = re.match(r'^(.+)_MAC$', k)
+ if m:
+ self.context_devname[v.lower()] = m.group(1)
def mac2ip(self, mac):
- components = mac.split(':')[2:]
- return [str(int(c, 16)) for c in components]
+ return '.'.join([str(int(c, 16)) for c in mac.split(':')[2:]])
- def get_ip(self, dev, components):
- var_name = dev.upper() + '_IP'
- if var_name in self.context:
- return self.context[var_name]
- else:
- return '.'.join(components)
+ def mac2network(self, mac):
+ return self.mac2ip(mac).rpartition(".")[0] + ".0"
- def get_mask(self, dev):
- var_name = dev.upper() + '_MASK'
- if var_name in self.context:
- return self.context[var_name]
- else:
- return '255.255.255.0'
+ def get_dns(self, dev):
+ return self.get_field(dev, "dns", "").split()
- def get_network(self, dev, components):
- var_name = dev.upper() + '_NETWORK'
- if var_name in self.context:
- return self.context[var_name]
- else:
- return '.'.join(components[:-1]) + '.0'
+ def get_domain(self, dev):
+ return self.get_field(dev, "domain")
+
+ def get_ip(self, dev, mac):
+ return self.get_field(dev, "ip", self.mac2ip(mac))
def get_gateway(self, dev):
- var_name = dev.upper() + '_GATEWAY'
- if var_name in self.context:
- return self.context[var_name]
- else:
- return None
+ return self.get_field(dev, "gateway")
- def get_dns(self, dev):
- var_name = dev.upper() + '_DNS'
- if var_name in self.context:
- return self.context[var_name]
- else:
- return None
+ def get_mask(self, dev):
+ return self.get_field(dev, "mask", "255.255.255.0")
- def get_domain(self, dev):
- var_name = dev.upper() + '_DOMAIN'
- if var_name in self.context:
- return self.context[var_name]
- else:
- return None
+ def get_network(self, dev, mac):
+ return self.get_field(dev, "network", self.mac2network(mac))
+
+ def get_field(self, dev, name, default=None):
+ """return the field name in context for device dev.
+
+ context stores <dev>_<NAME> (example: eth0_DOMAIN).
+ an empty string for value will return default."""
+ val = self.context.get('_'.join((dev, name,)).upper())
+ # allow empty string to return the default.
+ return default if val in (None, "") else val
def gen_conf(self):
- global_dns = []
- if 'DNS' in self.context:
- global_dns.append(self.context['DNS'])
+ global_dns = self.context.get('DNS', "").split()
conf = []
conf.append('auto lo')
@@ -172,29 +179,31 @@ class OpenNebulaNetwork(object):
conf.append('')
for mac, dev in self.ifaces.items():
- ip_components = self.mac2ip(mac)
+ mac = mac.lower()
+
+ # c_dev stores name in context 'ETHX' for this device.
+ # dev stores the current system name.
+ c_dev = self.context_devname.get(mac, dev)
conf.append('auto ' + dev)
conf.append('iface ' + dev + ' inet static')
- conf.append(' address ' + self.get_ip(dev, ip_components))
- conf.append(' network ' + self.get_network(dev, ip_components))
- conf.append(' netmask ' + self.get_mask(dev))
+ conf.append(' #hwaddress %s' % mac)
+ conf.append(' address ' + self.get_ip(c_dev, mac))
+ conf.append(' network ' + self.get_network(c_dev, mac))
+ conf.append(' netmask ' + self.get_mask(c_dev))
- gateway = self.get_gateway(dev)
+ gateway = self.get_gateway(c_dev)
if gateway:
conf.append(' gateway ' + gateway)
- domain = self.get_domain(dev)
+ domain = self.get_domain(c_dev)
if domain:
conf.append(' dns-search ' + domain)
# add global DNS servers to all interfaces
- dns = self.get_dns(dev)
+ dns = self.get_dns(c_dev)
if global_dns or dns:
- all_dns = global_dns
- if dns:
- all_dns.append(dns)
- conf.append(' dns-nameservers ' + ' '.join(all_dns))
+ conf.append(' dns-nameservers ' + ' '.join(global_dns + dns))
conf.append('')
@@ -329,8 +338,9 @@ def read_context_disk_dir(source_dir, asuser=None):
try:
pwd.getpwnam(asuser)
except KeyError as e:
- raise BrokenContextDiskDir("configured user '%s' "
- "does not exist", asuser)
+ raise BrokenContextDiskDir(
+ "configured user '{user}' does not exist".format(
+ user=asuser))
try:
path = os.path.join(source_dir, 'context.sh')
content = util.load_file(path)
diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
index b64a7f24..e55a7638 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -24,6 +24,9 @@ DEFAULT_METADATA = {
class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
+
+ dsname = "OpenStack"
+
def __init__(self, sys_cfg, distro, paths):
super(DataSourceOpenStack, self).__init__(sys_cfg, distro, paths)
self.metadata_address = None
@@ -96,7 +99,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
self.metadata_address = url2base.get(avail_url)
return bool(avail_url)
- def get_data(self):
+ def _get_data(self):
try:
if not self.wait_for_metadata_service():
return False
diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py
index 3a8a8e8f..b0b19c93 100644
--- a/cloudinit/sources/DataSourceScaleway.py
+++ b/cloudinit/sources/DataSourceScaleway.py
@@ -169,6 +169,8 @@ def query_data_api(api_type, api_address, retries, timeout):
class DataSourceScaleway(sources.DataSource):
+ dsname = "Scaleway"
+
def __init__(self, sys_cfg, distro, paths):
super(DataSourceScaleway, self).__init__(sys_cfg, distro, paths)
@@ -184,7 +186,7 @@ class DataSourceScaleway(sources.DataSource):
self.retries = int(self.ds_cfg.get('retries', DEF_MD_RETRIES))
self.timeout = int(self.ds_cfg.get('timeout', DEF_MD_TIMEOUT))
- def get_data(self):
+ def _get_data(self):
if not on_scaleway():
return False
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
index 6c6902fd..86bfa5d8 100644
--- a/cloudinit/sources/DataSourceSmartOS.py
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -159,6 +159,9 @@ LEGACY_USER_D = "/var/db"
class DataSourceSmartOS(sources.DataSource):
+
+ dsname = "Joyent"
+
_unset = "_unset"
smartos_type = _unset
md_client = _unset
@@ -211,7 +214,7 @@ class DataSourceSmartOS(sources.DataSource):
os.rename('/'.join([svc_path, 'provisioning']),
'/'.join([svc_path, 'provision_success']))
- def get_data(self):
+ def _get_data(self):
self._init()
md = {}
diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
index 9a43fbee..a05ca2f6 100644
--- a/cloudinit/sources/__init__.py
+++ b/cloudinit/sources/__init__.py
@@ -10,9 +10,11 @@
import abc
import copy
+import json
import os
import six
+from cloudinit.atomic_helper import write_json
from cloudinit import importer
from cloudinit import log as logging
from cloudinit import type_utils
@@ -33,6 +35,12 @@ DEP_FILESYSTEM = "FILESYSTEM"
DEP_NETWORK = "NETWORK"
DS_PREFIX = 'DataSource'
+# File in which instance meta-data, user-data and vendor-data is written
+INSTANCE_JSON_FILE = 'instance-data.json'
+
+# Key which can be provide a cloud's official product name to cloud-init
+METADATA_CLOUD_NAME_KEY = 'cloud-name'
+
LOG = logging.getLogger(__name__)
@@ -40,12 +48,39 @@ class DataSourceNotFoundException(Exception):
pass
+def process_base64_metadata(metadata, key_path=''):
+ """Strip ci-b64 prefix and return metadata with base64-encoded-keys set."""
+ md_copy = copy.deepcopy(metadata)
+ md_copy['base64-encoded-keys'] = []
+ for key, val in metadata.items():
+ if key_path:
+ sub_key_path = key_path + '/' + key
+ else:
+ sub_key_path = key
+ if isinstance(val, str) and val.startswith('ci-b64:'):
+ md_copy['base64-encoded-keys'].append(sub_key_path)
+ md_copy[key] = val.replace('ci-b64:', '')
+ if isinstance(val, dict):
+ return_val = process_base64_metadata(val, sub_key_path)
+ md_copy['base64-encoded-keys'].extend(
+ return_val.pop('base64-encoded-keys'))
+ md_copy[key] = return_val
+ return md_copy
+
+
@six.add_metaclass(abc.ABCMeta)
class DataSource(object):
dsmode = DSMODE_NETWORK
default_locale = 'en_US.UTF-8'
+ # Datasource name needs to be set by subclasses to determine which
+ # cloud-config datasource key is loaded
+ dsname = '_undef'
+
+ # Cached cloud_name as determined by _get_cloud_name
+ _cloud_name = None
+
def __init__(self, sys_cfg, distro, paths, ud_proc=None):
self.sys_cfg = sys_cfg
self.distro = distro
@@ -56,17 +91,8 @@ class DataSource(object):
self.vendordata = None
self.vendordata_raw = None
- # find the datasource config name.
- # remove 'DataSource' from classname on front, and remove 'Net' on end.
- # Both Foo and FooNet sources expect config in cfg['sources']['Foo']
- name = type_utils.obj_name(self)
- if name.startswith(DS_PREFIX):
- name = name[len(DS_PREFIX):]
- if name.endswith('Net'):
- name = name[0:-3]
-
- self.ds_cfg = util.get_cfg_by_path(self.sys_cfg,
- ("datasource", name), {})
+ self.ds_cfg = util.get_cfg_by_path(
+ self.sys_cfg, ("datasource", self.dsname), {})
if not self.ds_cfg:
self.ds_cfg = {}
@@ -78,6 +104,51 @@ class DataSource(object):
def __str__(self):
return type_utils.obj_name(self)
+ def _get_standardized_metadata(self):
+ """Return a dictionary of standardized metadata keys."""
+ return {'v1': {
+ 'local-hostname': self.get_hostname(),
+ 'instance-id': self.get_instance_id(),
+ 'cloud-name': self.cloud_name,
+ 'region': self.region,
+ 'availability-zone': self.availability_zone}}
+
+ def get_data(self):
+ """Datasources implement _get_data to setup metadata and userdata_raw.
+
+ Minimally, the datasource should return a boolean True on success.
+ """
+ return_value = self._get_data()
+ json_file = os.path.join(self.paths.run_dir, INSTANCE_JSON_FILE)
+ if not return_value:
+ return return_value
+
+ instance_data = {
+ 'ds': {
+ 'meta-data': self.metadata,
+ 'user-data': self.get_userdata_raw(),
+ 'vendor-data': self.get_vendordata_raw()}}
+ instance_data.update(
+ self._get_standardized_metadata())
+ try:
+ # Process content base64encoding unserializable values
+ content = util.json_dumps(instance_data)
+ # Strip base64: prefix and return base64-encoded-keys
+ processed_data = process_base64_metadata(json.loads(content))
+ except TypeError as e:
+ LOG.warning('Error persisting instance-data.json: %s', str(e))
+ return return_value
+ except UnicodeDecodeError as e:
+ LOG.warning('Error persisting instance-data.json: %s', str(e))
+ return return_value
+ write_json(json_file, processed_data, mode=0o600)
+ return return_value
+
+ def _get_data(self):
+ raise NotImplementedError(
+ 'Subclasses of DataSource must implement _get_data which'
+ ' sets self.metadata, vendordata_raw and userdata_raw.')
+
def get_userdata(self, apply_filter=False):
if self.userdata is None:
self.userdata = self.ud_proc.process(self.get_userdata_raw())
@@ -91,6 +162,34 @@ class DataSource(object):
return self.vendordata
@property
+ def cloud_name(self):
+ """Return lowercase cloud name as determined by the datasource.
+
+ Datasource can determine or define its own cloud product name in
+ metadata.
+ """
+ if self._cloud_name:
+ return self._cloud_name
+ if self.metadata and self.metadata.get(METADATA_CLOUD_NAME_KEY):
+ cloud_name = self.metadata.get(METADATA_CLOUD_NAME_KEY)
+ if isinstance(cloud_name, six.string_types):
+ self._cloud_name = cloud_name.lower()
+ LOG.debug(
+ 'Ignoring metadata provided key %s: non-string type %s',
+ METADATA_CLOUD_NAME_KEY, type(cloud_name))
+ else:
+ self._cloud_name = self._get_cloud_name().lower()
+ return self._cloud_name
+
+ def _get_cloud_name(self):
+ """Return the datasource name as it frequently matches cloud name.
+
+ Should be overridden in subclasses which can run on multiple
+ cloud names, such as DatasourceEc2.
+ """
+ return self.dsname
+
+ @property
def launch_index(self):
if not self.metadata:
return None
@@ -161,8 +260,11 @@ class DataSource(object):
@property
def availability_zone(self):
- return self.metadata.get('availability-zone',
- self.metadata.get('availability_zone'))
+ top_level_az = self.metadata.get(
+ 'availability-zone', self.metadata.get('availability_zone'))
+ if top_level_az:
+ return top_level_az
+ return self.metadata.get('placement', {}).get('availability-zone')
@property
def region(self):
@@ -346,7 +448,7 @@ def find_source(sys_cfg, distro, paths, ds_deps, cfg_list, pkg_list, reporter):
# Return an ordered list of classes that match (if any)
def list_sources(cfg_list, depends, pkg_list):
src_list = []
- LOG.debug(("Looking for for data source in: %s,"
+ LOG.debug(("Looking for data source in: %s,"
" via packages %s that matches dependencies %s"),
cfg_list, pkg_list, depends)
for ds_name in cfg_list:
@@ -417,4 +519,5 @@ def list_from_depends(depends, ds_list):
ret_list.append(cls)
return ret_list
+
# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py
index 959b1bda..90c12df1 100644
--- a/cloudinit/sources/helpers/azure.py
+++ b/cloudinit/sources/helpers/azure.py
@@ -199,10 +199,10 @@ class WALinuxAgentShim(object):
' </Container>',
'</Health>'])
- def __init__(self, fallback_lease_file=None):
+ def __init__(self, fallback_lease_file=None, dhcp_options=None):
LOG.debug('WALinuxAgentShim instantiated, fallback_lease_file=%s',
fallback_lease_file)
- self.dhcpoptions = None
+ self.dhcpoptions = dhcp_options
self._endpoint = None
self.openssl_manager = None
self.values = {}
@@ -220,7 +220,8 @@ class WALinuxAgentShim(object):
@property
def endpoint(self):
if self._endpoint is None:
- self._endpoint = self.find_endpoint(self.lease_file)
+ self._endpoint = self.find_endpoint(self.lease_file,
+ self.dhcpoptions)
return self._endpoint
@staticmethod
@@ -274,7 +275,8 @@ class WALinuxAgentShim(object):
name = os.path.basename(hook_file).replace('.json', '')
dhcp_options[name] = json.loads(util.load_file((hook_file)))
except ValueError:
- raise ValueError("%s is not valid JSON data", hook_file)
+ raise ValueError(
+ '{_file} is not valid JSON data'.format(_file=hook_file))
return dhcp_options
@staticmethod
@@ -291,10 +293,14 @@ class WALinuxAgentShim(object):
return _value
@staticmethod
- def find_endpoint(fallback_lease_file=None):
+ def find_endpoint(fallback_lease_file=None, dhcp245=None):
value = None
- LOG.debug('Finding Azure endpoint from networkd...')
- value = WALinuxAgentShim._networkd_get_value_from_leases()
+ if dhcp245 is not None:
+ value = dhcp245
+ LOG.debug("Using Azure Endpoint from dhcp options")
+ if value is None:
+ LOG.debug('Finding Azure endpoint from networkd...')
+ value = WALinuxAgentShim._networkd_get_value_from_leases()
if value is None:
# Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json
# a dhclient exit hook that calls cloud-init-dhclient-hook
@@ -366,8 +372,9 @@ class WALinuxAgentShim(object):
LOG.info('Reported ready to Azure fabric.')
-def get_metadata_from_fabric(fallback_lease_file=None):
- shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file)
+def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None):
+ shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,
+ dhcp_options=dhcp_opts)
try:
return shim.register_with_azure_and_fetch_data()
finally:
diff --git a/cloudinit/sources/helpers/vmware/imc/config.py b/cloudinit/sources/helpers/vmware/imc/config.py
index 49d441db..2eaeff34 100644
--- a/cloudinit/sources/helpers/vmware/imc/config.py
+++ b/cloudinit/sources/helpers/vmware/imc/config.py
@@ -100,4 +100,8 @@ class Config(object):
"""Returns marker id."""
return self._configFile.get(Config.MARKERID, None)
+ @property
+ def custom_script_name(self):
+ """Return the name of custom (pre/post) script."""
+ return self._configFile.get(Config.CUSTOM_SCRIPT, None)
# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/vmware/imc/config_custom_script.py b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
new file mode 100644
index 00000000..a7d4ad91
--- /dev/null
+++ b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
@@ -0,0 +1,153 @@
+# Copyright (C) 2017 Canonical Ltd.
+# Copyright (C) 2017 VMware Inc.
+#
+# Author: Maitreyee Saikia <msaikia@vmware.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import logging
+import os
+import stat
+from textwrap import dedent
+
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+
+class CustomScriptNotFound(Exception):
+ pass
+
+
+class CustomScriptConstant(object):
+ RC_LOCAL = "/etc/rc.local"
+ POST_CUST_TMP_DIR = "/root/.customization"
+ POST_CUST_RUN_SCRIPT_NAME = "post-customize-guest.sh"
+ POST_CUST_RUN_SCRIPT = os.path.join(POST_CUST_TMP_DIR,
+ POST_CUST_RUN_SCRIPT_NAME)
+ POST_REBOOT_PENDING_MARKER = "/.guest-customization-post-reboot-pending"
+
+
+class RunCustomScript(object):
+ def __init__(self, scriptname, directory):
+ self.scriptname = scriptname
+ self.directory = directory
+ self.scriptpath = os.path.join(directory, scriptname)
+
+ def prepare_script(self):
+ if not os.path.exists(self.scriptpath):
+ raise CustomScriptNotFound("Script %s not found!! "
+ "Cannot execute custom script!"
+ % self.scriptpath)
+ # Strip any CR characters from the decoded script
+ util.load_file(self.scriptpath).replace("\r", "")
+ st = os.stat(self.scriptpath)
+ os.chmod(self.scriptpath, st.st_mode | stat.S_IEXEC)
+
+
+class PreCustomScript(RunCustomScript):
+ def execute(self):
+ """Executing custom script with precustomization argument."""
+ LOG.debug("Executing pre-customization script")
+ self.prepare_script()
+ util.subp(["/bin/sh", self.scriptpath, "precustomization"])
+
+
+class PostCustomScript(RunCustomScript):
+ def __init__(self, scriptname, directory):
+ super(PostCustomScript, self).__init__(scriptname, directory)
+ # Determine when to run custom script. When postreboot is True,
+ # the user uploaded script will run as part of rc.local after
+ # the machine reboots. This is determined by presence of rclocal.
+ # When postreboot is False, script will run as part of cloud-init.
+ self.postreboot = False
+
+ def _install_post_reboot_agent(self, rclocal):
+ """
+ Install post-reboot agent for running custom script after reboot.
+ As part of this process, we are editing the rclocal file to run a
+ VMware script, which in turn is resposible for handling the user
+ script.
+ @param: path to rc local.
+ """
+ LOG.debug("Installing post-reboot customization from %s to %s",
+ self.directory, rclocal)
+ if not self.has_previous_agent(rclocal):
+ LOG.info("Adding post-reboot customization agent to rc.local")
+ new_content = dedent("""
+ # Run post-reboot guest customization
+ /bin/sh %s
+ exit 0
+ """) % CustomScriptConstant.POST_CUST_RUN_SCRIPT
+ existing_rclocal = util.load_file(rclocal).replace('exit 0\n', '')
+ st = os.stat(rclocal)
+ # "x" flag should be set
+ mode = st.st_mode | stat.S_IEXEC
+ util.write_file(rclocal, existing_rclocal + new_content, mode)
+
+ else:
+ # We don't need to update rclocal file everytime a customization
+ # is requested. It just needs to be done for the first time.
+ LOG.info("Post-reboot guest customization agent is already "
+ "registered in rc.local")
+ LOG.debug("Installing post-reboot customization agent finished: %s",
+ self.postreboot)
+
+ def has_previous_agent(self, rclocal):
+ searchstring = "# Run post-reboot guest customization"
+ if searchstring in open(rclocal).read():
+ return True
+ return False
+
+ def find_rc_local(self):
+ """
+ Determine if rc local is present.
+ """
+ rclocal = ""
+ if os.path.exists(CustomScriptConstant.RC_LOCAL):
+ LOG.debug("rc.local detected.")
+ # resolving in case of symlink
+ rclocal = os.path.realpath(CustomScriptConstant.RC_LOCAL)
+ LOG.debug("rc.local resolved to %s", rclocal)
+ else:
+ LOG.warning("Can't find rc.local, post-customization "
+ "will be run before reboot")
+ return rclocal
+
+ def install_agent(self):
+ rclocal = self.find_rc_local()
+ if rclocal:
+ self._install_post_reboot_agent(rclocal)
+ self.postreboot = True
+
+ def execute(self):
+ """
+ This method executes post-customization script before or after reboot
+ based on the presence of rc local.
+ """
+ self.prepare_script()
+ self.install_agent()
+ if not self.postreboot:
+ LOG.warning("Executing post-customization script inline")
+ util.subp(["/bin/sh", self.scriptpath, "postcustomization"])
+ else:
+ LOG.debug("Scheduling custom script to run post reboot")
+ if not os.path.isdir(CustomScriptConstant.POST_CUST_TMP_DIR):
+ os.mkdir(CustomScriptConstant.POST_CUST_TMP_DIR)
+ # Script "post-customize-guest.sh" and user uploaded script are
+ # are present in the same directory and needs to copied to a temp
+ # directory to be executed post reboot. User uploaded script is
+ # saved as customize.sh in the temp directory.
+ # post-customize-guest.sh excutes customize.sh after reboot.
+ LOG.debug("Copying post-customization script")
+ util.copy(self.scriptpath,
+ CustomScriptConstant.POST_CUST_TMP_DIR + "/customize.sh")
+ LOG.debug("Copying script to run post-customization script")
+ util.copy(
+ os.path.join(self.directory,
+ CustomScriptConstant.POST_CUST_RUN_SCRIPT_NAME),
+ CustomScriptConstant.POST_CUST_RUN_SCRIPT)
+ LOG.info("Creating post-reboot pending marker")
+ util.ensure_file(CustomScriptConstant.POST_REBOOT_PENDING_MARKER)
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
index 2fb07c59..2d8900e2 100644
--- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
@@ -161,7 +161,7 @@ class NicConfigurator(object):
if nic.primary and v4.gateways:
self.ipv4PrimaryGateway = v4.gateways[0]
subnet.update({'gateway': self.ipv4PrimaryGateway})
- return [subnet]
+ return ([subnet], route_list)
# Add routes if there is no primary nic
if not self._primaryNic:
diff --git a/cloudinit/sources/tests/__init__.py b/cloudinit/sources/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/cloudinit/sources/tests/__init__.py
diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py
new file mode 100644
index 00000000..af151154
--- /dev/null
+++ b/cloudinit/sources/tests/test_init.py
@@ -0,0 +1,202 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import os
+import six
+import stat
+
+from cloudinit.helpers import Paths
+from cloudinit.sources import (
+ INSTANCE_JSON_FILE, DataSource)
+from cloudinit.tests.helpers import CiTestCase, skipIf
+from cloudinit.user_data import UserDataProcessor
+from cloudinit import util
+
+
+class DataSourceTestSubclassNet(DataSource):
+
+ dsname = 'MyTestSubclass'
+
+ def __init__(self, sys_cfg, distro, paths, custom_userdata=None):
+ super(DataSourceTestSubclassNet, self).__init__(
+ sys_cfg, distro, paths)
+ self._custom_userdata = custom_userdata
+
+ def _get_cloud_name(self):
+ return 'SubclassCloudName'
+
+ def _get_data(self):
+ self.metadata = {'availability_zone': 'myaz',
+ 'local-hostname': 'test-subclass-hostname',
+ 'region': 'myregion'}
+ if self._custom_userdata:
+ self.userdata_raw = self._custom_userdata
+ else:
+ self.userdata_raw = 'userdata_raw'
+ self.vendordata_raw = 'vendordata_raw'
+ return True
+
+
+class InvalidDataSourceTestSubclassNet(DataSource):
+ pass
+
+
+class TestDataSource(CiTestCase):
+
+ with_logs = True
+
+ def setUp(self):
+ super(TestDataSource, self).setUp()
+ self.sys_cfg = {'datasource': {'_undef': {'key1': False}}}
+ self.distro = 'distrotest' # generally should be a Distro object
+ self.paths = Paths({})
+ self.datasource = DataSource(self.sys_cfg, self.distro, self.paths)
+
+ def test_datasource_init(self):
+ """DataSource initializes metadata attributes, ds_cfg and ud_proc."""
+ self.assertEqual(self.paths, self.datasource.paths)
+ self.assertEqual(self.sys_cfg, self.datasource.sys_cfg)
+ self.assertEqual(self.distro, self.datasource.distro)
+ self.assertIsNone(self.datasource.userdata)
+ self.assertEqual({}, self.datasource.metadata)
+ self.assertIsNone(self.datasource.userdata_raw)
+ self.assertIsNone(self.datasource.vendordata)
+ self.assertIsNone(self.datasource.vendordata_raw)
+ self.assertEqual({'key1': False}, self.datasource.ds_cfg)
+ self.assertIsInstance(self.datasource.ud_proc, UserDataProcessor)
+
+ def test_datasource_init_gets_ds_cfg_using_dsname(self):
+ """Init uses DataSource.dsname for sourcing ds_cfg."""
+ sys_cfg = {'datasource': {'MyTestSubclass': {'key2': False}}}
+ distro = 'distrotest' # generally should be a Distro object
+ paths = Paths({})
+ datasource = DataSourceTestSubclassNet(sys_cfg, distro, paths)
+ self.assertEqual({'key2': False}, datasource.ds_cfg)
+
+ def test_str_is_classname(self):
+ """The string representation of the datasource is the classname."""
+ self.assertEqual('DataSource', str(self.datasource))
+ self.assertEqual(
+ 'DataSourceTestSubclassNet',
+ str(DataSourceTestSubclassNet('', '', self.paths)))
+
+ def test__get_data_unimplemented(self):
+ """Raise an error when _get_data is not implemented."""
+ with self.assertRaises(NotImplementedError) as context_manager:
+ self.datasource.get_data()
+ self.assertIn(
+ 'Subclasses of DataSource must implement _get_data',
+ str(context_manager.exception))
+ datasource2 = InvalidDataSourceTestSubclassNet(
+ self.sys_cfg, self.distro, self.paths)
+ with self.assertRaises(NotImplementedError) as context_manager:
+ datasource2.get_data()
+ self.assertIn(
+ 'Subclasses of DataSource must implement _get_data',
+ str(context_manager.exception))
+
+ def test_get_data_calls_subclass__get_data(self):
+ """Datasource.get_data uses the subclass' version of _get_data."""
+ tmp = self.tmp_dir()
+ datasource = DataSourceTestSubclassNet(
+ self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
+ self.assertTrue(datasource.get_data())
+ self.assertEqual(
+ {'availability_zone': 'myaz',
+ 'local-hostname': 'test-subclass-hostname',
+ 'region': 'myregion'},
+ datasource.metadata)
+ self.assertEqual('userdata_raw', datasource.userdata_raw)
+ self.assertEqual('vendordata_raw', datasource.vendordata_raw)
+
+ def test_get_data_write_json_instance_data(self):
+ """get_data writes INSTANCE_JSON_FILE to run_dir as readonly root."""
+ tmp = self.tmp_dir()
+ datasource = DataSourceTestSubclassNet(
+ self.sys_cfg, self.distro, Paths({'run_dir': tmp}))
+ datasource.get_data()
+ json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
+ content = util.load_file(json_file)
+ expected = {
+ 'base64-encoded-keys': [],
+ 'v1': {
+ 'availability-zone': 'myaz',
+ 'cloud-name': 'subclasscloudname',
+ 'instance-id': 'iid-datasource',
+ 'local-hostname': 'test-subclass-hostname',
+ 'region': 'myregion'},
+ 'ds': {
+ 'meta-data': {'availability_zone': 'myaz',
+ 'local-hostname': 'test-subclass-hostname',
+ 'region': 'myregion'},
+ 'user-data': 'userdata_raw',
+ 'vendor-data': 'vendordata_raw'}}
+ self.assertEqual(expected, util.load_json(content))
+ file_stat = os.stat(json_file)
+ self.assertEqual(0o600, stat.S_IMODE(file_stat.st_mode))
+
+ def test_get_data_handles_redacted_unserializable_content(self):
+ """get_data warns unserializable content in INSTANCE_JSON_FILE."""
+ tmp = self.tmp_dir()
+ datasource = DataSourceTestSubclassNet(
+ self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
+ custom_userdata={'key1': 'val1', 'key2': {'key2.1': self.paths}})
+ self.assertTrue(datasource.get_data())
+ json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
+ content = util.load_file(json_file)
+ expected_userdata = {
+ 'key1': 'val1',
+ 'key2': {
+ 'key2.1': "Warning: redacted unserializable type <class"
+ " 'cloudinit.helpers.Paths'>"}}
+ instance_json = util.load_json(content)
+ self.assertEqual(
+ expected_userdata, instance_json['ds']['user-data'])
+
+ @skipIf(not six.PY3, "json serialization on <= py2.7 handles bytes")
+ def test_get_data_base64encodes_unserializable_bytes(self):
+ """On py3, get_data base64encodes any unserializable content."""
+ tmp = self.tmp_dir()
+ datasource = DataSourceTestSubclassNet(
+ self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
+ custom_userdata={'key1': 'val1', 'key2': {'key2.1': b'\x123'}})
+ self.assertTrue(datasource.get_data())
+ json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
+ content = util.load_file(json_file)
+ instance_json = util.load_json(content)
+ self.assertEqual(
+ ['ds/user-data/key2/key2.1'],
+ instance_json['base64-encoded-keys'])
+ self.assertEqual(
+ {'key1': 'val1', 'key2': {'key2.1': 'EjM='}},
+ instance_json['ds']['user-data'])
+
+ @skipIf(not six.PY2, "json serialization on <= py2.7 handles bytes")
+ def test_get_data_handles_bytes_values(self):
+ """On py2 get_data handles bytes values without having to b64encode."""
+ tmp = self.tmp_dir()
+ datasource = DataSourceTestSubclassNet(
+ self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
+ custom_userdata={'key1': 'val1', 'key2': {'key2.1': b'\x123'}})
+ self.assertTrue(datasource.get_data())
+ json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
+ content = util.load_file(json_file)
+ instance_json = util.load_json(content)
+ self.assertEqual([], instance_json['base64-encoded-keys'])
+ self.assertEqual(
+ {'key1': 'val1', 'key2': {'key2.1': '\x123'}},
+ instance_json['ds']['user-data'])
+
+ @skipIf(not six.PY2, "Only python2 hits UnicodeDecodeErrors on non-utf8")
+ def test_non_utf8_encoding_logs_warning(self):
+ """When non-utf-8 values exist in py2 instance-data is not written."""
+ tmp = self.tmp_dir()
+ datasource = DataSourceTestSubclassNet(
+ self.sys_cfg, self.distro, Paths({'run_dir': tmp}),
+ custom_userdata={'key1': 'val1', 'key2': {'key2.1': b'ab\xaadef'}})
+ self.assertTrue(datasource.get_data())
+ json_file = self.tmp_path(INSTANCE_JSON_FILE, tmp)
+ self.assertFalse(os.path.exists(json_file))
+ self.assertIn(
+ "WARNING: Error persisting instance-data.json: 'utf8' codec can't"
+ " decode byte 0xaa in position 2: invalid start byte",
+ self.logs.getvalue())