diff options
author | Scott Moser <smoser@brickies.net> | 2017-03-03 02:26:38 -0500 |
---|---|---|
committer | Scott Moser <smoser@brickies.net> | 2017-03-03 02:26:38 -0500 |
commit | d7004bcf269fe60e456de336ecda9a9d2fe50bfd (patch) | |
tree | cf49b1fbc06388d46fa435814d24d97c83476047 | |
parent | 1de8720effd029727bb5ef7972e7e4d859a1b53a (diff) | |
parent | c81ea53bbdc4ada9d2b52430e106aeb3c38b4e0a (diff) | |
download | vyos-cloud-init-d7004bcf269fe60e456de336ecda9a9d2fe50bfd.tar.gz vyos-cloud-init-d7004bcf269fe60e456de336ecda9a9d2fe50bfd.zip |
merge from master at 0.7.9-47-gc81ea53
44 files changed, 2367 insertions, 377 deletions
@@ -27,13 +27,16 @@ ifeq ($(distro),) distro = redhat endif -READ_VERSION=$(shell $(PYVER) $(CWD)/tools/read-version) +READ_VERSION=$(shell $(PYVER) $(CWD)/tools/read-version || \ + echo read-version-failed) CODE_VERSION=$(shell $(PYVER) -c "from cloudinit import version; print(version.version_string())") all: check -check: check_version pep8 $(pyflakes) test $(yaml) +check: check_version test $(yaml) + +style-check: pep8 $(pyflakes) pep8: @$(CWD)/tools/run-pep8 @@ -62,8 +65,8 @@ test: $(unittests) check_version: @if [ "$(READ_VERSION)" != "$(CODE_VERSION)" ]; then \ - echo "Error: read-version version $(READ_VERSION)" \ - "not equal to code version $(CODE_VERSION)"; exit 2; \ + echo "Error: read-version version '$(READ_VERSION)'" \ + "not equal to code version '$(CODE_VERSION)'"; exit 2; \ else true; fi clean_pyc: @@ -73,7 +76,7 @@ clean: clean_pyc rm -rf /var/log/cloud-init.log /var/lib/cloud/ yaml: - @$(CWD)/tools/validate-yaml.py $(YAML_FILES) + @$(PYVER) $(CWD)/tools/validate-yaml.py $(YAML_FILES) rpm: ./packages/brpm --distro $(distro) @@ -83,3 +86,4 @@ deb: .PHONY: test pyflakes pyflakes3 clean pep8 rpm deb yaml check_version .PHONY: pip-test-requirements pip-requirements clean_pyc unittest unittest3 +.PHONY: style-check diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index c83496c5..6ff4e1c0 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -26,8 +26,10 @@ from cloudinit import signal_handler from cloudinit import sources from cloudinit import stages from cloudinit import templater +from cloudinit import url_helper from cloudinit import util from cloudinit import version +from cloudinit import warnings from cloudinit import reporting from cloudinit.reporting import events @@ -129,23 +131,104 @@ def apply_reporting_cfg(cfg): reporting.update_configuration(cfg.get('reporting')) +def parse_cmdline_url(cmdline, names=('cloud-config-url', 'url')): + data = util.keyval_str_to_dict(cmdline) + for key in names: + if key in data: + return key, data[key] + raise KeyError("No keys (%s) found in string '%s'" % + (cmdline, names)) + + +def attempt_cmdline_url(path, network=True, cmdline=None): + """Write data from url referenced in command line to path. + + path: a file to write content to if downloaded. + network: should network access be assumed. + cmdline: the cmdline to parse for cloud-config-url. + + This is used in MAAS datasource, in "ephemeral" (read-only root) + environment where the instance netboots to iscsi ro root. + and the entity that controls the pxe config has to configure + the maas datasource. + + An attempt is made on network urls even in local datasource + for case of network set up in initramfs. + + Return value is a tuple of a logger function (logging.DEBUG) + and a message indicating what happened. + """ + + if cmdline is None: + cmdline = util.get_cmdline() + + try: + cmdline_name, url = parse_cmdline_url(cmdline) + except KeyError: + return (logging.DEBUG, "No kernel command line url found.") + + path_is_local = url.startswith("file://") or url.startswith("/") + + if path_is_local and os.path.exists(path): + if network: + m = ("file '%s' existed, possibly from local stage download" + " of command line url '%s'. Not re-writing." % (path, url)) + level = logging.INFO + if path_is_local: + level = logging.DEBUG + else: + m = ("file '%s' existed, possibly from previous boot download" + " of command line url '%s'. Not re-writing." % (path, url)) + level = logging.WARN + + return (level, m) + + kwargs = {'url': url, 'timeout': 10, 'retries': 2} + if network or path_is_local: + level = logging.WARN + kwargs['sec_between'] = 1 + else: + level = logging.DEBUG + kwargs['sec_between'] = .1 + + data = None + header = b'#cloud-config' + try: + resp = util.read_file_or_url(**kwargs) + if resp.ok(): + data = resp.contents + if not resp.contents.startswith(header): + if cmdline_name == 'cloud-config-url': + level = logging.WARN + else: + level = logging.INFO + return ( + level, + "contents of '%s' did not start with %s" % (url, header)) + else: + return (level, + "url '%s' returned code %s. Ignoring." % (url, resp.code)) + + except url_helper.UrlError as e: + return (level, "retrieving url '%s' failed: %s" % (url, e)) + + util.write_file(path, data, mode=0o600) + return (logging.INFO, + "wrote cloud-config data from %s='%s' to %s" % + (cmdline_name, url, path)) + + def main_init(name, args): deps = [sources.DEP_FILESYSTEM, sources.DEP_NETWORK] if args.local: deps = [sources.DEP_FILESYSTEM] - if not args.local: - # See doc/kernel-cmdline.txt - # - # This is used in maas datasource, in "ephemeral" (read-only root) - # environment where the instance netboots to iscsi ro root. - # and the entity that controls the pxe config has to configure - # the maas datasource. - # - # Could be used elsewhere, only works on network based (not local). - root_name = "%s.d" % (CLOUD_CONFIG) - target_fn = os.path.join(root_name, "91_kernel_cmdline_url.cfg") - util.read_write_cmdline_url(target_fn) + early_logs = [] + early_logs.append( + attempt_cmdline_url( + path=os.path.join("%s.d" % CLOUD_CONFIG, + "91_kernel_cmdline_url.cfg"), + network=not args.local)) # Cloud-init 'init' stage is broken up into the following sub-stages # 1. Ensure that the init object fetches its config without errors @@ -171,12 +254,14 @@ def main_init(name, args): outfmt = None errfmt = None try: - LOG.debug("Closing stdin") + early_logs.append((logging.DEBUG, "Closing stdin.")) util.close_stdin() (outfmt, errfmt) = util.fixup_output(init.cfg, name) except Exception: - util.logexc(LOG, "Failed to setup output redirection!") - print_exc("Failed to setup output redirection!") + msg = "Failed to setup output redirection!" + util.logexc(LOG, msg) + print_exc(msg) + early_logs.append((logging.WARN, msg)) if args.debug: # Reset so that all the debug handlers are closed out LOG.debug(("Logging being reset, this logger may no" @@ -190,6 +275,10 @@ def main_init(name, args): # been redirected and log now configured. welcome(name, msg=w_msg) + # re-play early log messages before logging was setup + for lvl, msg in early_logs: + LOG.log(lvl, msg) + # Stage 3 try: init.initialize() @@ -224,8 +313,15 @@ def main_init(name, args): " would allow us to stop early.") else: existing = "check" - if util.get_cfg_option_bool(init.cfg, 'manual_cache_clean', False): + mcfg = util.get_cfg_option_bool(init.cfg, 'manual_cache_clean', False) + if mcfg: + LOG.debug("manual cache clean set from config") existing = "trust" + else: + mfile = path_helper.get_ipath_cur("manual_clean_marker") + if os.path.exists(mfile): + LOG.debug("manual cache clean found from marker: %s", mfile) + existing = "trust" init.purge_cache() # Delete the non-net file as well @@ -318,10 +414,48 @@ def main_init(name, args): # give the activated datasource a chance to adjust init.activate_datasource() + di_report_warn(datasource=init.datasource, cfg=init.cfg) + # Stage 10 return (init.datasource, run_module_section(mods, name, name)) +def di_report_warn(datasource, cfg): + if 'di_report' not in cfg: + LOG.debug("no di_report found in config.") + return + + dicfg = cfg.get('di_report', {}) + if not isinstance(dicfg, dict): + LOG.warn("di_report config not a dictionary: %s", dicfg) + return + + dslist = dicfg.get('datasource_list') + if dslist is None: + LOG.warn("no 'datasource_list' found in di_report.") + return + elif not isinstance(dslist, list): + LOG.warn("di_report/datasource_list not a list: %s", dslist) + return + + # ds.__module__ is like cloudinit.sources.DataSourceName + # where Name is the thing that shows up in datasource_list. + modname = datasource.__module__.rpartition(".")[2] + if modname.startswith(sources.DS_PREFIX): + modname = modname[len(sources.DS_PREFIX):] + else: + LOG.warn("Datasource '%s' came from unexpected module '%s'.", + datasource, modname) + + if modname in dslist: + LOG.debug("used datasource '%s' from '%s' was in di_report's list: %s", + datasource, modname, dslist) + return + + warnings.show_warning('dsid_missing_source', cfg, + source=modname, dslist=str(dslist)) + + def main_modules(action_name, args): name = args.mode # Cloud-init 'modules' stages are broken up into the following sub-stages diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py index e42799f9..aa3dfe5f 100644 --- a/cloudinit/config/cc_set_hostname.py +++ b/cloudinit/config/cc_set_hostname.py @@ -27,7 +27,7 @@ will be used. **Config keys**:: - perserve_hostname: <true/false> + preserve_hostname: <true/false> fqdn: <fqdn> hostname: <fqdn/hostname> """ diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index aa558381..7498c63a 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -190,13 +190,18 @@ class Distro(distros.Distro): if pkgs is None: pkgs = [] - cmd = ['yum'] - # If enabled, then yum will be tolerant of errors on the command line - # with regard to packages. - # For example: if you request to install foo, bar and baz and baz is - # installed; yum won't error out complaining that baz is already - # installed. - cmd.append("-t") + if util.which('dnf'): + LOG.debug('Using DNF for package management') + cmd = ['dnf'] + else: + LOG.debug('Using YUM for package management') + # the '-t' argument makes yum tolerant of errors on the command + # line with regard to packages. + # + # For example: if you request to install foo, bar and baz and baz + # is installed; yum won't error out complaining that baz is already + # installed. + cmd = ['yum', '-t'] # Determines whether or not yum prompts for confirmation # of critical actions. We don't want to prompt... cmd.append("-y") diff --git a/cloudinit/ec2_utils.py b/cloudinit/ec2_utils.py index c656ef14..13691549 100644 --- a/cloudinit/ec2_utils.py +++ b/cloudinit/ec2_utils.py @@ -28,7 +28,7 @@ class MetadataLeafDecoder(object): def __call__(self, field, blob): if not blob: - return blob + return '' try: blob = util.decode_binary(blob) except UnicodeDecodeError: @@ -82,6 +82,9 @@ class MetadataMaterializer(object): field_name = get_name(field) if not field or not field_name: continue + # Don't materialize credentials + if field_name == 'security-credentials': + continue if has_children(field): if field_name not in children: children.append(field_name) diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py index 4528fb01..7435d58d 100644 --- a/cloudinit/helpers.py +++ b/cloudinit/helpers.py @@ -339,6 +339,8 @@ class Paths(object): "vendordata_raw": "vendor-data.txt", "vendordata": "vendor-data.txt.i", "instance_id": ".instance-id", + "manual_clean_marker": "manual-clean", + "warnings": "warnings", } # Set when a datasource becomes active self.datasource = ds diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index b06ffac9..5b249f1f 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -90,8 +90,6 @@ def _iface_add_attrs(iface, index): def _iface_start_entry(iface, index, render_hwaddress=False): fullname = iface['name'] - if index != 0: - fullname += ":%s" % index control = iface['control'] if control == "auto": @@ -113,6 +111,16 @@ def _iface_start_entry(iface, index, render_hwaddress=False): return lines +def _subnet_is_ipv6(subnet): + # 'static6' or 'dhcp6' + if subnet['type'].endswith('6'): + # This is a request for DHCPv6. + return True + elif subnet['type'] == 'static' and ":" in subnet['address']: + return True + return False + + def _parse_deb_config_data(ifaces, contents, src_dir, src_path): """Parses the file contents, placing result into ifaces. @@ -354,21 +362,23 @@ class Renderer(renderer.Renderer): sections = [] subnets = iface.get('subnets', {}) if subnets: - for index, subnet in zip(range(0, len(subnets)), subnets): + for index, subnet in enumerate(subnets): iface['index'] = index iface['mode'] = subnet['type'] iface['control'] = subnet.get('control', 'auto') subnet_inet = 'inet' - if iface['mode'].endswith('6'): - # This is a request for DHCPv6. - subnet_inet += '6' - elif iface['mode'] == 'static' and ":" in subnet['address']: - # This is a static IPv6 address. + if _subnet_is_ipv6(subnet): subnet_inet += '6' iface['inet'] = subnet_inet - if iface['mode'].startswith('dhcp'): + if subnet['type'].startswith('dhcp'): iface['mode'] = 'dhcp' + # do not emit multiple 'auto $IFACE' lines as older (precise) + # ifupdown complains + if True in ["auto %s" % (iface['name']) in line + for line in sections]: + iface['control'] = 'alias' + lines = list( _iface_start_entry( iface, index, render_hwaddress=render_hwaddress) + @@ -378,11 +388,6 @@ class Renderer(renderer.Renderer): for route in subnet.get('routes', []): lines.extend(self._render_route(route, indent=" ")) - if len(subnets) > 1 and index == 0: - tmpl = " post-up ifup %s:%s\n" - for i in range(1, len(subnets)): - lines.append(tmpl % (iface['name'], i)) - sections.append(lines) else: # ifenslave docs say to auto the slave devices diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 9be74070..6e7739fb 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -282,12 +282,12 @@ class Renderer(renderer.Renderer): if len(iface_subnets) == 1: cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0]) elif len(iface_subnets) > 1: - for i, iface_subnet in enumerate(iface_subnets, - start=len(iface.children)): + for i, isubnet in enumerate(iface_subnets, + start=len(iface_cfg.children)): iface_sub_cfg = iface_cfg.copy() iface_sub_cfg.name = "%s:%s" % (iface_name, i) - iface.children.append(iface_sub_cfg) - cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet) + iface_cfg.children.append(iface_sub_cfg) + cls._render_subnet(iface_sub_cfg, route_cfg, isubnet) @classmethod def _render_bond_interfaces(cls, network_state, iface_contents): diff --git a/cloudinit/settings.py b/cloudinit/settings.py index b1fdd31f..692ff5e5 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py @@ -14,6 +14,8 @@ CFG_ENV_NAME = "CLOUD_CFG" # This is expected to be a yaml formatted file CLOUD_CONFIG = '/etc/cloud/cloud.cfg' +RUN_CLOUD_CONFIG = '/run/cloud-init/cloud.cfg' + # What u get if no config is provided CFG_BUILTIN = { 'datasource_list': [ diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py index 2d00255c..9debe947 100644 --- a/cloudinit/sources/DataSourceAliYun.py +++ b/cloudinit/sources/DataSourceAliYun.py @@ -22,6 +22,10 @@ class DataSourceAliYun(EC2.DataSourceEc2): def get_public_ssh_keys(self): return parse_public_keys(self.metadata.get('public-keys', {})) + @property + def cloud_platform(self): + return EC2.Platforms.ALIYUN + def parse_public_keys(public_keys): keys = [] diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index c657fd09..6f01a139 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -16,18 +16,31 @@ from cloudinit import log as logging from cloudinit import sources from cloudinit import url_helper as uhelp from cloudinit import util +from cloudinit import warnings LOG = logging.getLogger(__name__) # Which version we are requesting of the ec2 metadata apis DEF_MD_VERSION = '2009-04-04' +STRICT_ID_PATH = ("datasource", "Ec2", "strict_id") +STRICT_ID_DEFAULT = "warn" + + +class Platforms(object): + ALIYUN = "AliYun" + AWS = "AWS" + BRIGHTBOX = "Brightbox" + SEEDED = "Seeded" + UNKNOWN = "Unknown" + class DataSourceEc2(sources.DataSource): # 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 metadata_urls = ["http://169.254.169.254", "http://instance-data.:8773"] + _cloud_platform = None def __init__(self, sys_cfg, distro, paths): sources.DataSource.__init__(self, sys_cfg, distro, paths) @@ -41,8 +54,18 @@ class DataSourceEc2(sources.DataSource): self.userdata_raw = seed_ret['user-data'] self.metadata = seed_ret['meta-data'] LOG.debug("Using seeded ec2 data from %s", self.seed_dir) + self._cloud_platform = Platforms.SEEDED return True + strict_mode, _sleep = read_strict_mode( + util.get_cfg_by_path(self.sys_cfg, STRICT_ID_PATH, + STRICT_ID_DEFAULT), ("warn", None)) + + LOG.debug("strict_mode: %s, cloud_platform=%s", + strict_mode, self.cloud_platform) + if strict_mode == "true" and self.cloud_platform == Platforms.UNKNOWN: + return False + try: if not self.wait_for_metadata_service(): return False @@ -51,8 +74,8 @@ class DataSourceEc2(sources.DataSource): ec2.get_instance_userdata(self.api_ver, self.metadata_address) self.metadata = ec2.get_instance_metadata(self.api_ver, self.metadata_address) - LOG.debug("Crawl of metadata service took %s seconds", - int(time.time() - start_time)) + LOG.debug("Crawl of metadata service took %.3f seconds", + time.time() - start_time) return True except Exception: util.logexc(LOG, "Failed reading from metadata address %s", @@ -190,6 +213,126 @@ class DataSourceEc2(sources.DataSource): return az[:-1] return None + @property + def cloud_platform(self): + if self._cloud_platform is None: + self._cloud_platform = identify_platform() + return self._cloud_platform + + def activate(self, cfg, is_new_instance): + if not is_new_instance: + return + if self.cloud_platform == Platforms.UNKNOWN: + warn_if_necessary( + util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT), + cfg) + + +def read_strict_mode(cfgval, default): + try: + return parse_strict_mode(cfgval) + except ValueError as e: + LOG.warn(e) + return default + + +def parse_strict_mode(cfgval): + # given a mode like: + # true, false, warn,[sleep] + # return tuple with string mode (true|false|warn) and sleep. + if cfgval is True: + return 'true', None + if cfgval is False: + return 'false', None + + if not cfgval: + return 'warn', 0 + + mode, _, sleep = cfgval.partition(",") + if mode not in ('true', 'false', 'warn'): + raise ValueError( + "Invalid mode '%s' in strict_id setting '%s': " + "Expected one of 'true', 'false', 'warn'." % (mode, cfgval)) + + if sleep: + try: + sleep = int(sleep) + except ValueError: + raise ValueError("Invalid sleep '%s' in strict_id setting '%s': " + "not an integer" % (sleep, cfgval)) + else: + sleep = None + + return mode, sleep + + +def warn_if_necessary(cfgval, cfg): + try: + mode, sleep = parse_strict_mode(cfgval) + except ValueError as e: + LOG.warn(e) + return + + if mode == "false": + return + + warnings.show_warning('non_ec2_md', cfg, mode=True, sleep=sleep) + + +def identify_aws(data): + # data is a dictionary returned by _collect_platform_data. + if (data['uuid'].startswith('ec2') and + (data['uuid_source'] == 'hypervisor' or + data['uuid'] == data['serial'])): + return Platforms.AWS + + return None + + +def identify_brightbox(data): + if data['serial'].endswith('brightbox.com'): + return Platforms.BRIGHTBOX + + +def identify_platform(): + # identify the platform and return an entry in Platforms. + data = _collect_platform_data() + checks = (identify_aws, identify_brightbox, lambda x: Platforms.UNKNOWN) + for checker in checks: + try: + result = checker(data) + if result: + return result + except Exception as e: + LOG.warn("calling %s with %s raised exception: %s", + checker, data, e) + + +def _collect_platform_data(): + # returns a dictionary with all lower case values: + # uuid: system-uuid from dmi or /sys/hypervisor + # uuid_source: 'hypervisor' (/sys/hypervisor/uuid) or 'dmi' + # serial: dmi 'system-serial-number' (/sys/.../product_serial) + data = {} + try: + uuid = util.load_file("/sys/hypervisor/uuid").strip() + data['uuid_source'] = 'hypervisor' + except Exception: + uuid = util.read_dmi_data('system-uuid') + data['uuid_source'] = 'dmi' + + if uuid is None: + uuid = '' + data['uuid'] = uuid.lower() + + serial = util.read_dmi_data('system-serial-number') + if serial is None: + serial = '' + + data['serial'] = serial.lower() + + return data + # Used to match classes to dependencies datasources = [ diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py index 78928c77..d70784ac 100644 --- a/cloudinit/sources/DataSourceOVF.py +++ b/cloudinit/sources/DataSourceOVF.py @@ -48,6 +48,7 @@ class DataSourceOVF(sources.DataSource): self.environment = None self.cfg = {} self.supported_seed_starts = ("/", "file://") + self.vmware_customization_supported = True def __str__(self): root = sources.DataSource.__str__(self) @@ -78,7 +79,10 @@ class DataSourceOVF(sources.DataSource): found.append(seed) elif system_type and 'vmware' in system_type.lower(): LOG.debug("VMware Virtualization Platform found") - if not util.get_cfg_option_bool( + if not self.vmware_customization_supported: + LOG.debug("Skipping the check for " + "VMware Customization support") + elif not util.get_cfg_option_bool( self.sys_cfg, "disable_vmware_customization", True): deployPkgPluginPath = search_file("/usr/lib/vmware-tools", "libdeployPkgPlugin.so") @@ -90,17 +94,18 @@ class DataSourceOVF(sources.DataSource): # copies the customization specification file to # /var/run/vmware-imc directory. cloud-init code needs # to search for the file in that directory. + max_wait = get_max_wait_from_cfg(self.ds_cfg) vmwareImcConfigFilePath = util.log_time( logfunc=LOG.debug, msg="waiting for configuration file", func=wait_for_imc_cfg_file, - args=("/var/run/vmware-imc", "cust.cfg")) + args=("/var/run/vmware-imc", "cust.cfg", max_wait)) if vmwareImcConfigFilePath: - LOG.debug("Found VMware DeployPkg Config File at %s" % + LOG.debug("Found VMware Customization Config File at %s", vmwareImcConfigFilePath) else: - LOG.debug("Did not find VMware DeployPkg Config File Path") + LOG.debug("Did not find VMware Customization Config File") else: LOG.debug("Customization for VMware platform is disabled.") @@ -206,6 +211,29 @@ class DataSourceOVFNet(DataSourceOVF): DataSourceOVF.__init__(self, sys_cfg, distro, paths) self.seed_dir = os.path.join(paths.seed_dir, 'ovf-net') self.supported_seed_starts = ("http://", "https://", "ftp://") + self.vmware_customization_supported = False + + +def get_max_wait_from_cfg(cfg): + default_max_wait = 90 + max_wait_cfg_option = 'vmware_cust_file_max_wait' + max_wait = default_max_wait + + if not cfg: + return max_wait + + try: + max_wait = int(cfg.get(max_wait_cfg_option, default_max_wait)) + except ValueError: + LOG.warn("Failed to get '%s', using %s", + max_wait_cfg_option, default_max_wait) + + if max_wait <= 0: + LOG.warn("Invalid value '%s' for '%s', using '%s' instead", + max_wait, max_wait_cfg_option, default_max_wait) + max_wait = default_max_wait + + return max_wait def wait_for_imc_cfg_file(dirpath, filename, maxwait=180, naplen=5): @@ -215,6 +243,7 @@ def wait_for_imc_cfg_file(dirpath, filename, maxwait=180, naplen=5): fileFullPath = search_file(dirpath, filename) if fileFullPath: return fileFullPath + LOG.debug("Waiting for VMware Customization Config File") time.sleep(naplen) waited += naplen return None diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py index 2a58f1cd..e1ea21f8 100644 --- a/cloudinit/sources/DataSourceOpenStack.py +++ b/cloudinit/sources/DataSourceOpenStack.py @@ -45,6 +45,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): # max_wait < 0 indicates do not wait max_wait = -1 timeout = 10 + retries = 5 try: max_wait = int(self.ds_cfg.get("max_wait", max_wait)) @@ -55,7 +56,13 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): timeout = max(0, int(self.ds_cfg.get("timeout", timeout))) except Exception: util.logexc(LOG, "Failed to get timeout, using %s", timeout) - return (max_wait, timeout) + + try: + retries = int(self.ds_cfg.get("retries", retries)) + except Exception: + util.logexc(LOG, "Failed to get max wait. using %s", retries) + + return (max_wait, timeout, retries) def wait_for_metadata_service(self): urls = self.ds_cfg.get("metadata_urls", [DEF_MD_URL]) @@ -76,7 +83,7 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): md_urls.append(md_url) url2base[md_url] = url - (max_wait, timeout) = self._get_url_settings() + (max_wait, timeout, retries) = self._get_url_settings() start_time = time.time() avail_url = url_helper.wait_for_url(urls=md_urls, max_wait=max_wait, timeout=timeout) @@ -89,13 +96,15 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): self.metadata_address = url2base.get(avail_url) return bool(avail_url) - def get_data(self, retries=5, timeout=5): + def get_data(self): try: if not self.wait_for_metadata_service(): return False except IOError: return False + (max_wait, timeout, retries) = self._get_url_settings() + try: results = util.log_time(LOG.debug, 'Crawl of openstack metadata service', diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py index d5a7c346..67ac21db 100644 --- a/cloudinit/sources/helpers/vmware/imc/config_nic.py +++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py @@ -101,7 +101,11 @@ class NicConfigurator(object): return lines # Static Ipv4 - v4 = nic.staticIpv4 + addrs = nic.staticIpv4 + if not addrs: + return lines + + v4 = addrs[0] if v4.ip: lines.append(' address %s' % v4.ip) if v4.netmask: @@ -197,22 +201,6 @@ class NicConfigurator(object): util.subp(["pkill", "dhclient"], rcs=[0, 1]) util.subp(["rm", "-f", "/var/lib/dhcp/*"]) - def if_down_up(self): - names = [] - for nic in self.nics: - name = self.mac2Name.get(nic.mac.lower()) - names.append(name) - - for name in names: - logger.info('Bring down interface %s' % name) - util.subp(["ifdown", "%s" % name]) - - self.clear_dhcp() - - for name in names: - logger.info('Bring up interface %s' % name) - util.subp(["ifup", "%s" % name]) - def configure(self): """ Configure the /etc/network/intefaces @@ -232,6 +220,6 @@ class NicConfigurator(object): for line in lines: fp.write('%s\n' % line) - self.if_down_up() + self.clear_dhcp() # vi: ts=4 expandtab diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py index be8a49e8..b95b956f 100644 --- a/cloudinit/ssh_util.py +++ b/cloudinit/ssh_util.py @@ -22,8 +22,11 @@ DEF_SSHD_CFG = "/etc/ssh/sshd_config" VALID_KEY_TYPES = ( "dsa", "ecdsa", + "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp521-cert-v01@openssh.com", "ed25519", "rsa", diff --git a/cloudinit/stages.py b/cloudinit/stages.py index b0552dde..5bed9032 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -11,7 +11,8 @@ import sys import six from six.moves import cPickle as pickle -from cloudinit.settings import (PER_INSTANCE, FREQUENCIES, CLOUD_CONFIG) +from cloudinit.settings import ( + FREQUENCIES, CLOUD_CONFIG, PER_INSTANCE, RUN_CLOUD_CONFIG) from cloudinit import handlers @@ -188,6 +189,12 @@ class Init(object): def _write_to_cache(self): if self.datasource is NULL_DATA_SOURCE: return False + if util.get_cfg_option_bool(self.cfg, 'manual_cache_clean', False): + # The empty file in instance/ dir indicates manual cleaning, + # and can be read by ds-identify. + util.write_file( + self.paths.get_ipath_cur("manual_clean_marker"), + omode="w", content="") return _pkl_store(self.datasource, self.paths.get_ipath_cur("obj_pkl")) def _get_datasources(self): @@ -828,6 +835,10 @@ class Modules(object): return self._run_modules(mostly_mods) +def read_runtime_config(): + return util.read_conf(RUN_CLOUD_CONFIG) + + def fetch_base_config(): return util.mergemanydict( [ @@ -835,6 +846,8 @@ def fetch_base_config(): util.get_builtin_cfg(), # Anything in your conf.d or 'default' cloud.cfg location. util.read_conf_with_confd(CLOUD_CONFIG), + # runtime config + read_runtime_config(), # Kernel/cmdline parameters override system config util.read_conf_from_cmdline(), ], reverse=True) diff --git a/cloudinit/util.py b/cloudinit/util.py index 5725129e..7196a7ca 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -1089,31 +1089,6 @@ def get_fqdn_from_hosts(hostname, filename="/etc/hosts"): return fqdn -def get_cmdline_url(names=('cloud-config-url', 'url'), - starts=b"#cloud-config", cmdline=None): - if cmdline is None: - cmdline = get_cmdline() - - data = keyval_str_to_dict(cmdline) - url = None - key = None - for key in names: - if key in data: - url = data[key] - break - - if not url: - return (None, None, None) - - resp = read_file_or_url(url) - # allow callers to pass starts as text when comparing to bytes contents - starts = encode_text(starts) - if resp.ok() and resp.contents.startswith(starts): - return (key, url, resp.contents) - - return (key, url, None) - - def is_resolvable(name): """determine if a url is resolvable, return a boolean This also attempts to be resilent against dns redirection. @@ -1475,25 +1450,6 @@ def ensure_dirs(dirlist, mode=0o755): ensure_dir(d, mode) -def read_write_cmdline_url(target_fn): - if not os.path.exists(target_fn): - try: - (key, url, content) = get_cmdline_url() - except Exception: - logexc(LOG, "Failed fetching command line url") - return - try: - if key and content: - write_file(target_fn, content, mode=0o600) - LOG.debug(("Wrote to %s with contents of command line" - " url %s (len=%s)"), target_fn, url, len(content)) - elif key and not content: - LOG.debug(("Command line key %s with url" - " %s had no contents"), key, url) - except Exception: - logexc(LOG, "Failed writing url content to %s", target_fn) - - def yaml_dumps(obj, explicit_start=True, explicit_end=True): return yaml.safe_dump(obj, line_break="\n", diff --git a/cloudinit/warnings.py b/cloudinit/warnings.py new file mode 100644 index 00000000..3206d4e9 --- /dev/null +++ b/cloudinit/warnings.py @@ -0,0 +1,139 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit import helpers +from cloudinit import log as logging +from cloudinit import util + +import os +import time + +LOG = logging.getLogger() + +WARNINGS = { + 'non_ec2_md': """ +This system is using the EC2 Metadata Service, but does not appear to +be running on Amazon EC2 or one of cloud-init's known platforms that +provide a EC2 Metadata service. In the future, cloud-init may stop +reading metadata from the EC2 Metadata Service unless the platform can +be identified. + +If you are seeing this message, please file a bug against +cloud-init at + https://bugs.launchpad.net/cloud-init/+filebug?field.tags=dsid +Make sure to include the cloud provider your instance is +running on. + +For more information see + https://bugs.launchpad.net/bugs/1660385 + +After you have filed a bug, you can disable this warning by +launching your instance with the cloud-config below, or +putting that content into + /etc/cloud/cloud.cfg.d/99-ec2-datasource.cfg + +#cloud-config +datasource: + Ec2: + strict_id: false""", + 'dsid_missing_source': """ +A new feature in cloud-init identified possible datasources for +this system as: + {dslist} +However, the datasource used was: {source} + +In the future, cloud-init will only attempt to use datasources that +are identified or specifically configured. +For more information see + https://bugs.launchpad.net/bugs/1669675 + +If you are seeing this message, please file a bug against +cloud-init at + https://bugs.launchpad.net/cloud-init/+filebug?field.tags=dsid +Make sure to include the cloud provider your instance is +running on. + +After you have filed a bug, you can disable this warning by launching +your instance with the cloud-config below, or putting that content +into /etc/cloud/cloud.cfg.d/99-warnings.cfg + +#cloud-config +warnings: + dsid_missing_source: off""", +} + + +def _get_warn_dir(cfg): + paths = helpers.Paths( + path_cfgs=cfg.get('system_info', {}).get('paths', {})) + return paths.get_ipath_cur('warnings') + + +def _load_warn_cfg(cfg, name, mode=True, sleep=None): + # parse cfg['warnings']['name'] returning boolean, sleep + # expected value is form of: + # (on|off|true|false|sleep)[,sleeptime] + # boolean True == on, False == off + default = (mode, sleep) + if not cfg or not isinstance(cfg, dict): + return default + + ncfg = util.get_cfg_by_path(cfg, ('warnings', name)) + if ncfg is None: + return default + + if ncfg in ("on", "true", True): + return True, None + + if ncfg in ("off", "false", False): + return False, None + + mode, _, csleep = ncfg.partition(",") + if mode != "sleep": + return default + + if csleep: + try: + sleep = int(csleep) + except ValueError: + return default + + return True, sleep + + +def show_warning(name, cfg=None, sleep=None, mode=True, **kwargs): + # kwargs are used for .format of the message. + # sleep and mode are default values used if + # cfg['warnings']['name'] is not present. + if cfg is None: + cfg = {} + + mode, sleep = _load_warn_cfg(cfg, name, mode=mode, sleep=sleep) + if not mode: + return + + msg = WARNINGS[name].format(**kwargs) + msgwidth = 70 + linewidth = msgwidth + 4 + + fmt = "# %%-%ds #" % msgwidth + topline = "*" * linewidth + "\n" + fmtlines = [] + for line in msg.strip("\n").splitlines(): + fmtlines.append(fmt % line) + + closeline = topline + if sleep: + sleepmsg = " [sleeping for %d seconds] " % sleep + closeline = sleepmsg.center(linewidth, "*") + "\n" + + util.write_file( + os.path.join(_get_warn_dir(cfg), name), + topline + "\n".join(fmtlines) + "\n" + topline) + + LOG.warn(topline + "\n".join(fmtlines) + "\n" + closeline) + + if sleep: + LOG.debug("sleeping %d seconds for warning '%s'" % (sleep, name)) + time.sleep(sleep) + +# vi: ts=4 expandtab diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt index c5f84b13..c03f1026 100644 --- a/doc/examples/cloud-config.txt +++ b/doc/examples/cloud-config.txt @@ -200,7 +200,7 @@ ssh_import_id: [smoser] # # Default: none # -debconf_selections: | # Need to perserve newlines +debconf_selections: | # Need to preserve newlines # Force debconf priority to critical. debconf debconf/priority select critical diff --git a/doc/rtd/topics/datasources/altcloud.rst b/doc/rtd/topics/datasources/altcloud.rst index 8646e77e..202b0a4a 100644 --- a/doc/rtd/topics/datasources/altcloud.rst +++ b/doc/rtd/topics/datasources/altcloud.rst @@ -66,7 +66,7 @@ NOTE: The file name on the ISO must be: ``user-data.txt`` .. sourcecode:: sh - % cp simple_scirpt.bash my-iso/user-data.txt + % cp simple_script.bash my-iso/user-data.txt % genisoimage -o user-data.iso -r my-iso Verify the ISO @@ -75,7 +75,7 @@ Verify the ISO .. sourcecode:: sh % sudo mkdir /media/vsphere_iso - % sudo mount -o loop JoeV_CI_02.iso /media/vsphere_iso + % sudo mount -o loop user-data.iso /media/vsphere_iso % cat /media/vsphere_iso/user-data.txt % sudo umount /media/vsphere_iso diff --git a/doc/rtd/topics/datasources/openstack.rst b/doc/rtd/topics/datasources/openstack.rst index ea47ea85..164b0e0c 100644 --- a/doc/rtd/topics/datasources/openstack.rst +++ b/doc/rtd/topics/datasources/openstack.rst @@ -1,7 +1,41 @@ OpenStack ========= -*TODO* +This datasource supports reading data from the +`OpenStack Metadata Service +<http://docs.openstack.org/admin-guide/compute-networking-nova.html#metadata-service>`_. + +Configuration +------------- +The following configuration can be set for the datasource in system +configuration (in `/etc/cloud/cloud.cfg` or `/etc/cloud/cloud.cfg.d/`). + +The settings that may be configured are: + + * **metadata_urls**: This list of urls will be searched for an OpenStack + metadata service. The first entry that successfully returns a 200 response + for <url>/openstack will be selected. (default: ['http://169.254.169.254']). + * **max_wait**: the maximum amount of clock time in seconds that should be + spent searching metadata_urls. A value less than zero will result in only + one request being made, to the first in the list. (default: -1) + * **timeout**: the timeout value provided to urlopen for each individual http + request. This is used both when selecting a metadata_url and when crawling + the metadata service. (default: 10) + * **retries**: The number of retries that should be done for an http request. + This value is used only after metadata_url is selected. (default: 5) + +An example configuration with the default values is provided as example below: + +.. sourcecode:: yaml + + #cloud-config + datasource: + OpenStack: + metadata_urls: ["http://169.254.169.254"] + max_wait: -1 + timeout: 10 + retries: 5 + Vendor Data ----------- diff --git a/doc/rtd/topics/format.rst b/doc/rtd/topics/format.rst index ed87d3ed..436eb00f 100644 --- a/doc/rtd/topics/format.rst +++ b/doc/rtd/topics/format.rst @@ -127,11 +127,11 @@ Begins with: ``#cloud-boothook`` or ``Content-Type: text/cloud-boothook`` when u Part Handler ============ -This is a ``part-handler``. It will be written to a file in ``/var/lib/cloud/data`` based on its filename (which is generated). -This must be python code that contains a ``list_types`` method and a ``handle_type`` method. -Once the section is read the ``list_types`` method will be called. It must return a list of mime-types that this part-handler handles. +This is a ``part-handler``: It contains custom code for either supporting new mime-types in multi-part user data, or overriding the existing handlers for supported mime-types. It will be written to a file in ``/var/lib/cloud/data`` based on its filename (which is generated). +This must be python code that contains a ``list_types`` function and a ``handle_part`` function. +Once the section is read the ``list_types`` method will be called. It must return a list of mime-types that this part-handler handles. Because mime parts are processed in order, a ``part-handler`` part must precede any parts with mime-types it is expected to handle in the same user data. -The ``handle_type`` method must be like: +The ``handle_part`` function must be defined like: .. code-block:: python @@ -141,8 +141,9 @@ The ``handle_type`` method must be like: # filename = the filename of the part (or a generated filename if none is present in mime data) # payload = the parts' content -Cloud-init will then call the ``handle_type`` method once at begin, once per part received, and once at end. -The ``begin`` and ``end`` calls are to allow the part handler to do initialization or teardown. +Cloud-init will then call the ``handle_part`` function once before it handles any parts, once per part received, and once after all parts have been handled. +The ``'__begin__'`` and ``'__end__'`` sentinels allow the part handler to do initialization or teardown before or after +receiving any parts. Begins with: ``#part-handler`` or ``Content-Type: text/part-handler`` when using a MIME archive. diff --git a/packages/debian/rules.in b/packages/debian/rules.in index 9b004357..053b7649 100755 --- a/packages/debian/rules.in +++ b/packages/debian/rules.in @@ -11,6 +11,8 @@ override_dh_install: dh_install install -d debian/cloud-init/etc/rsyslog.d cp tools/21-cloudinit.conf debian/cloud-init/etc/rsyslog.d/21-cloudinit.conf + install -D ./tools/Z99-cloud-locale-test.sh debian/cloud-init/etc/profile.d/Z99-cloud-locale-test.sh + install -D ./tools/Z99-cloudinit-warnings.sh debian/cloud-init/etc/profile.d/Z99-cloudinit-warnings.sh override_dh_auto_test: ifeq (,$(findstring nocheck,$(DEB_BUILD_OPTIONS))) @@ -168,7 +168,8 @@ else: (ETC + '/cloud/templates', glob('templates/*')), (ETC + '/NetworkManager/dispatcher.d/', ['tools/hook-network-manager']), (ETC + '/dhcp/dhclient-exit-hooks.d/', ['tools/hook-dhclient']), - (USR_LIB_EXEC + '/cloud-init', ['tools/uncloud-init', + (USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify', + 'tools/uncloud-init', 'tools/write-ssh-key-fingerprints']), (USR + '/share/doc/cloud-init', [f for f in glob('doc/*') if is_f(f)]), (USR + '/share/doc/cloud-init/examples', diff --git a/systemd/cloud-init-generator b/systemd/cloud-init-generator index fedb6309..bd9f2678 100755 --- a/systemd/cloud-init-generator +++ b/systemd/cloud-init-generator @@ -6,6 +6,8 @@ DEBUG_LEVEL=1 LOG_D="/run/cloud-init" ENABLE="enabled" DISABLE="disabled" +FOUND="found" +NOTFOUND="notfound" RUN_ENABLED_FILE="$LOG_D/$ENABLE" CLOUD_SYSTEM_TARGET="/lib/systemd/system/cloud-init.target" CLOUD_TARGET_NAME="cloud-init.target" @@ -74,10 +76,30 @@ default() { _RET="$ENABLE" } +check_for_datasource() { + local ds_rc="" dsidentify="/usr/lib/cloud-init/ds-identify" + if [ ! -x "$dsidentify" ]; then + debug 1 "no ds-identify in $dsidentify. _RET=$FOUND" + return 0 + fi + $dsidentify + ds_rc=$? + debug 1 "ds-identify rc=$ds_rc" + if [ "$ds_rc" = "0" ]; then + _RET="$FOUND" + debug 1 "ds-identify _RET=$_RET" + return 0 + fi + _RET="$NOTFOUND" + debug 1 "ds-identify _RET=$_RET" + return 1 +} + main() { local normal_d="$1" early_d="$2" late_d="$3" local target_name="multi-user.target" gen_d="$early_d" local link_path="$gen_d/${target_name}.wants/${CLOUD_TARGET_NAME}" + local ds="$NOTFOUND" debug 1 "$0 normal=$normal_d early=$early_d late=$late_d" debug 2 "$0 $*" @@ -93,7 +115,20 @@ main() { debug 0 "search $search returned $ret" fi done - + + # enable AND ds=found == enable + # enable AND ds=notfound == disable + # disable || <any> == disabled + if [ "$result" = "$ENABLE" ]; then + debug 1 "checking for datasource" + check_for_datasource + ds=$_RET + if [ "$ds" = "$NOTFOUND" ]; then + debug 1 "cloud-init is enabled but no datasource found, disabling" + result="$DISABLE" + fi + fi + if [ "$result" = "$ENABLE" ]; then if [ -e "$link_path" ]; then debug 1 "already enabled: no change needed" @@ -124,7 +159,7 @@ main() { rm -f "$RUN_ENABLED_FILE" fi else - debug 0 "unexpected result '$result'" + debug 0 "unexpected result '$result' 'ds=$ds'" ret=3 fi return $ret diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index cf3b46d2..90e2431f 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -29,7 +29,6 @@ PY2 = False PY26 = False PY27 = False PY3 = False -FIX_HTTPRETTY = False _PY_VER = sys.version_info _PY_MAJOR, _PY_MINOR, _PY_MICRO = _PY_VER[0:3] @@ -44,8 +43,6 @@ else: PY2 = True if (_PY_MAJOR, _PY_MINOR) >= (3, 0): PY3 = True - if _PY_MINOR == 4 and _PY_MICRO < 3: - FIX_HTTPRETTY = True # Makes the old path start @@ -86,6 +83,28 @@ class TestCase(unittest2.TestCase): pass +class CiTestCase(TestCase): + """This is the preferred test case base class unless user + needs other test case classes below.""" + def tmp_dir(self, dir=None, cleanup=True): + # return a full path to a temporary directory that will be cleaned up. + if dir is None: + tmpd = tempfile.mkdtemp( + prefix="ci-%s." % self.__class__.__name__) + else: + tmpd = tempfile.mkdtemp(dir=dir) + self.addCleanup(functools.partial(shutil.rmtree, tmpd)) + return tmpd + + def tmp_path(self, path, dir=None): + # return an absolute path to 'path' under dir. + # if dir is None, one will be created with tmp_dir() + # the file is not created or modified. + if dir is None: + dir = self.tmp_dir() + return os.path.normpath(os.path.abspath(os.path.join(dir, path))) + + class ResourceUsingTestCase(TestCase): def setUp(self): super(ResourceUsingTestCase, self).setUp() @@ -216,37 +235,6 @@ class FilesystemMockingTestCase(ResourceUsingTestCase): return root -def import_httpretty(): - """Import HTTPretty and monkey patch Python 3.4 issue. - See https://github.com/gabrielfalcao/HTTPretty/pull/193 and - as well as https://github.com/gabrielfalcao/HTTPretty/issues/221. - - Lifted from - https://github.com/inveniosoftware/datacite/blob/master/tests/helpers.py - """ - if not FIX_HTTPRETTY: - import httpretty - else: - import socket - old_SocketType = socket.SocketType - - import httpretty - from httpretty import core - - def sockettype_patch(f): - @functools.wraps(f) - def inner(*args, **kwargs): - f(*args, **kwargs) - socket.SocketType = old_SocketType - socket.__dict__['SocketType'] = old_SocketType - return inner - - core.httpretty.disable = sockettype_patch( - httpretty.httpretty.disable - ) - return httpretty - - class HttprettyTestCase(TestCase): # necessary as http_proxy gets in the way of httpretty # https://github.com/gabrielfalcao/HTTPretty/issues/122 @@ -262,23 +250,10 @@ class HttprettyTestCase(TestCase): super(HttprettyTestCase, self).tearDown() -class TempDirTestCase(TestCase): - # provide a tempdir per class, not per test. - def setUp(self): - super(TempDirTestCase, self).setUp() - self.tmp = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp) - - def tmp_path(self, path): - if path.startswith(os.path.sep): - path = "." + path - - return os.path.normpath(os.path.join(self.tmp, path)) - - def populate_dir(path, files): if not os.path.exists(path): os.makedirs(path) + ret = [] for (name, content) in files.items(): p = os.path.join(path, name) util.ensure_dir(os.path.dirname(p)) @@ -288,6 +263,9 @@ def populate_dir(path, files): else: fp.write(content.encode('utf-8')) fp.close() + ret.append(p) + + return ret def dir2dict(startdir, prefix=None): diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py index 7b6f8c4e..781f6d54 100644 --- a/tests/unittests/test__init__.py +++ b/tests/unittests/test__init__.py @@ -1,16 +1,18 @@ # This file is part of cloud-init. See LICENSE file for license information. +import logging import os import shutil import tempfile +from cloudinit.cmd import main from cloudinit import handlers from cloudinit import helpers from cloudinit import settings from cloudinit import url_helper from cloudinit import util -from .helpers import TestCase, ExitStack, mock +from .helpers import TestCase, CiTestCase, ExitStack, mock class FakeModule(handlers.Handler): @@ -170,44 +172,68 @@ class TestHandlerHandlePart(TestCase): self.data, self.ctype, self.filename, self.payload) -class TestCmdlineUrl(TestCase): - def test_invalid_content(self): - url = "http://example.com/foo" - key = "mykey" - payload = b"0" - cmdline = "ro %s=%s bar=1" % (key, url) +class TestCmdlineUrl(CiTestCase): + def test_parse_cmdline_url_nokey_raises_keyerror(self): + self.assertRaises( + KeyError, main.parse_cmdline_url, 'root=foo bar single') - with mock.patch('cloudinit.url_helper.readurl', - return_value=url_helper.StringResponse(payload)): - self.assertEqual( - util.get_cmdline_url(names=[key], starts="xxxxxx", - cmdline=cmdline), - (key, url, None)) + def test_parse_cmdline_url_found(self): + cmdline = 'root=foo bar single url=http://example.com arg1 -v' + self.assertEqual( + ('url', 'http://example.com'), main.parse_cmdline_url(cmdline)) - def test_valid_content(self): - url = "http://example.com/foo" - key = "mykey" - payload = b"xcloud-config\nmydata: foo\nbar: wark\n" + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_invalid_content(self, m_read): + key = "cloud-config-url" + url = 'http://example.com/foo' cmdline = "ro %s=%s bar=1" % (key, url) + m_read.return_value = url_helper.StringResponse(b"unexpected blob") - with mock.patch('cloudinit.url_helper.readurl', - return_value=url_helper.StringResponse(payload)): - self.assertEqual( - util.get_cmdline_url(names=[key], starts=b"xcloud-config", - cmdline=cmdline), - (key, url, payload)) + fpath = self.tmp_path("ccfile") + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + self.assertEqual(logging.WARN, lvl) + self.assertIn(url, msg) + self.assertFalse(os.path.exists(fpath)) - def test_no_key_found(self): + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_valid_content(self, m_read): url = "http://example.com/foo" - key = "mykey" - cmdline = "ro %s=%s bar=1" % (key, url) - - with mock.patch('cloudinit.url_helper.readurl', - return_value=url_helper.StringResponse(b'')): - self.assertEqual( - util.get_cmdline_url(names=["does-not-appear"], - starts="#cloud-config", cmdline=cmdline), - (None, None, None)) + payload = b"#cloud-config\nmydata: foo\nbar: wark\n" + cmdline = "ro %s=%s bar=1" % ('cloud-config-url', url) + + m_read.return_value = url_helper.StringResponse(payload) + fpath = self.tmp_path("ccfile") + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + self.assertEqual(util.load_file(fpath, decode=False), payload) + self.assertEqual(logging.INFO, lvl) + self.assertIn(url, msg) + + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_no_key_found(self, m_read): + cmdline = "ro mykey=http://example.com/foo root=foo" + fpath = self.tmp_path("ccpath") + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + + m_read.assert_not_called() + self.assertFalse(os.path.exists(fpath)) + self.assertEqual(logging.DEBUG, lvl) + + @mock.patch('cloudinit.cmd.main.util.read_file_or_url') + def test_exception_warns(self, m_read): + url = "http://example.com/foo" + cmdline = "ro cloud-config-url=%s root=LABEL=bar" % url + fpath = self.tmp_path("ccfile") + m_read.side_effect = url_helper.UrlError( + cause="Unexpected Error", url="http://example.com/foo") + + lvl, msg = main.attempt_cmdline_url( + fpath, network=True, cmdline=cmdline) + self.assertEqual(logging.WARN, lvl) + self.assertIn(url, msg) + self.assertFalse(os.path.exists(fpath)) # vi: ts=4 expandtab diff --git a/tests/unittests/test_atomic_helper.py b/tests/unittests/test_atomic_helper.py index e170c7c3..515919d8 100644 --- a/tests/unittests/test_atomic_helper.py +++ b/tests/unittests/test_atomic_helper.py @@ -6,10 +6,10 @@ import stat from cloudinit import atomic_helper -from . import helpers +from .helpers import CiTestCase -class TestAtomicHelper(helpers.TempDirTestCase): +class TestAtomicHelper(CiTestCase): def test_basic_usage(self): """write_file takes bytes if no omode.""" path = self.tmp_path("test_basic_usage") diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index 4092d9ca..4ad86bb6 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -564,12 +564,12 @@ class TestConvertString(helpers.TestCase): class TestFetchBaseConfig(helpers.TestCase): - - def test_only_builtin_gets_builtin2(self): + def test_only_builtin_gets_builtin(self): ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_with_confd': None, - 'read_conf_from_cmdline': None}, + 'cloudinit.stages', + {'util.read_conf_with_confd': None, + 'util.read_conf_from_cmdline': None, + 'read_runtime_config': {'return_value': {}}}, stages.fetch_base_config) self.assertEqual(util.get_builtin_cfg(), ret) @@ -578,9 +578,11 @@ class TestFetchBaseConfig(helpers.TestCase): test_key = sorted(builtin)[0] test_value = 'test' ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_with_confd': {'return_value': {test_key: test_value}}, - 'read_conf_from_cmdline': None}, + 'cloudinit.stages', + {'util.read_conf_with_confd': + {'return_value': {test_key: test_value}}, + 'util.read_conf_from_cmdline': None, + 'read_runtime_config': {'return_value': {}}}, stages.fetch_base_config) self.assertEqual(ret.get(test_key), test_value) builtin[test_key] = test_value @@ -592,25 +594,44 @@ class TestFetchBaseConfig(helpers.TestCase): test_value = 'test' cmdline = {test_key: test_value} ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_from_cmdline': {'return_value': cmdline}, - 'read_conf_with_confd': None}, + 'cloudinit.stages', + {'util.read_conf_from_cmdline': {'return_value': cmdline}, + 'util.read_conf_with_confd': None, + 'read_runtime_config': None}, stages.fetch_base_config) self.assertEqual(ret.get(test_key), test_value) builtin[test_key] = test_value self.assertEqual(ret, builtin) - def test_cmdline_overrides_conf_d_and_defaults(self): + def test_cmdline_overrides_confd_runtime_and_defaults(self): builtin = {'key1': 'value0', 'key3': 'other2'} conf_d = {'key1': 'value1', 'key2': 'other1'} cmdline = {'key3': 'other3', 'key2': 'other2'} + runtime = {'key3': 'runtime3'} ret = helpers.wrap_and_call( - 'cloudinit.stages.util', - {'read_conf_with_confd': {'return_value': conf_d}, - 'get_builtin_cfg': {'return_value': builtin}, - 'read_conf_from_cmdline': {'return_value': cmdline}}, + 'cloudinit.stages', + {'util.read_conf_with_confd': {'return_value': conf_d}, + 'util.get_builtin_cfg': {'return_value': builtin}, + 'read_runtime_config': {'return_value': runtime}, + 'util.read_conf_from_cmdline': {'return_value': cmdline}}, stages.fetch_base_config) self.assertEqual(ret, {'key1': 'value1', 'key2': 'other2', 'key3': 'other3'}) + def test_order_precedence_is_builtin_system_runtime_cmdline(self): + builtin = {'key1': 'builtin0', 'key3': 'builtin3'} + conf_d = {'key1': 'confd1', 'key2': 'confd2', 'keyconfd1': 'kconfd1'} + runtime = {'key1': 'runtime1', 'key2': 'runtime2'} + cmdline = {'key1': 'cmdline1'} + ret = helpers.wrap_and_call( + 'cloudinit.stages', + {'util.read_conf_with_confd': {'return_value': conf_d}, + 'util.get_builtin_cfg': {'return_value': builtin}, + 'util.read_conf_from_cmdline': {'return_value': cmdline}, + 'read_runtime_config': {'return_value': runtime}, + }, + stages.fetch_base_config) + self.assertEqual(ret, {'key1': 'cmdline1', 'key2': 'runtime2', + 'key3': 'builtin3', 'keyconfd1': 'kconfd1'}) + # vi: ts=4 expandtab diff --git a/tests/unittests/test_datasource/test_gce.py b/tests/unittests/test_datasource/test_gce.py index 4f667678..4f83454e 100644 --- a/tests/unittests/test_datasource/test_gce.py +++ b/tests/unittests/test_datasource/test_gce.py @@ -4,6 +4,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. +import httpretty import re from base64 import b64encode, b64decode @@ -15,7 +16,6 @@ from cloudinit.sources import DataSourceGCE from .. import helpers as test_helpers -httpretty = test_helpers.import_httpretty() GCE_META = { 'instance/id': '123', @@ -59,6 +59,8 @@ def _set_mock_metadata(gce_meta=None): else: return (404, headers, '') + # reset is needed. https://github.com/gabrielfalcao/HTTPretty/issues/316 + httpretty.reset() httpretty.register_uri(httpretty.GET, MD_URL_RE, body=_request_callback) diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py index e5b6fcc6..7bf55084 100644 --- a/tests/unittests/test_datasource/test_openstack.py +++ b/tests/unittests/test_datasource/test_openstack.py @@ -5,6 +5,7 @@ # This file is part of cloud-init. See LICENSE file for license information. import copy +import httpretty as hp import json import re @@ -20,8 +21,6 @@ from cloudinit.sources import DataSourceOpenStack as ds from cloudinit.sources.helpers import openstack from cloudinit import util -hp = test_helpers.import_httpretty() - BASE_URL = "http://169.254.169.254" PUBKEY = u'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460\n' EC2_META = { @@ -232,7 +231,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): None, helpers.Paths({})) self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertTrue(found) self.assertEqual(2, ds_os.version) md = dict(ds_os.metadata) @@ -256,7 +255,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): None, helpers.Paths({})) self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertFalse(found) self.assertIsNone(ds_os.version) @@ -275,7 +274,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): 'timeout': 0, } self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertFalse(found) self.assertIsNone(ds_os.version) @@ -298,7 +297,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase): 'timeout': 0, } self.assertIsNone(ds_os.version) - found = ds_os.get_data(timeout=0.1, retries=0) + found = ds_os.get_data() self.assertFalse(found) self.assertIsNone(ds_os.version) diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py index 88746e0a..88746e0a 100755..100644 --- a/tests/unittests/test_distros/test_user_data_normalize.py +++ b/tests/unittests/test_distros/test_user_data_normalize.py diff --git a/tests/unittests/test_ec2_util.py b/tests/unittests/test_ec2_util.py index 4a33d747..65fdb519 100644 --- a/tests/unittests/test_ec2_util.py +++ b/tests/unittests/test_ec2_util.py @@ -1,12 +1,12 @@ # This file is part of cloud-init. See LICENSE file for license information. +import httpretty as hp + from . import helpers from cloudinit import ec2_utils as eu from cloudinit import url_helper as uh -hp = helpers.import_httpretty() - class TestEc2Util(helpers.HttprettyTestCase): VERSION = 'latest' @@ -140,4 +140,49 @@ class TestEc2Util(helpers.HttprettyTestCase): self.assertEqual(bdm['ami'], 'sdb') self.assertEqual(bdm['ephemeral0'], 'sdc') + @hp.activate + def test_metadata_no_security_credentials(self): + base_url = 'http://169.254.169.254/%s/meta-data/' % (self.VERSION) + hp.register_uri(hp.GET, base_url, status=200, + body="\n".join(['instance-id', + 'iam/'])) + hp.register_uri(hp.GET, uh.combine_url(base_url, 'instance-id'), + status=200, body='i-0123451689abcdef0') + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/'), + status=200, + body="\n".join(['info/', 'security-credentials/'])) + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/info/'), + status=200, + body='LastUpdated') + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/info/LastUpdated'), + status=200, body='2016-10-27T17:29:39Z') + hp.register_uri(hp.GET, + uh.combine_url(base_url, 'iam/security-credentials/'), + status=200, + body='ReadOnly/') + hp.register_uri(hp.GET, + uh.combine_url(base_url, + 'iam/security-credentials/ReadOnly/'), + status=200, + body="\n".join(['LastUpdated', 'Expiration'])) + hp.register_uri(hp.GET, + uh.combine_url( + base_url, + 'iam/security-credentials/ReadOnly/LastUpdated'), + status=200, body='2016-10-27T17:28:17Z') + hp.register_uri(hp.GET, + uh.combine_url( + base_url, + 'iam/security-credentials/ReadOnly/Expiration'), + status=200, body='2016-10-28T00:00:34Z') + md = eu.get_instance_metadata(self.VERSION, retries=0, timeout=0.1) + self.assertEqual(md['instance-id'], 'i-0123451689abcdef0') + iam = md['iam'] + self.assertEqual(1, len(iam)) + self.assertEqual(iam['info']['LastUpdated'], '2016-10-27T17:29:39Z') + self.assertNotIn('security-credentials', iam) + # vi: ts=4 expandtab diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 1090282a..4b03ff72 100755..100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -8,11 +8,10 @@ from cloudinit.net import sysconfig from cloudinit.sources.helpers import openstack from cloudinit import util +from .helpers import CiTestCase from .helpers import dir2dict from .helpers import mock from .helpers import populate_dir -from .helpers import TempDirTestCase -from .helpers import TestCase import base64 import copy @@ -20,8 +19,6 @@ import gzip import io import json import os -import shutil -import tempfile import textwrap import yaml @@ -166,6 +163,91 @@ nameserver 172.19.0.12 ('etc/udev/rules.d/70-persistent-net.rules', "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))] + }, + { + 'in_data': { + "services": [{"type": "dns", "address": "172.19.0.12"}], + "networks": [{ + "network_id": "public-ipv4", + "type": "ipv4", "netmask": "255.255.252.0", + "link": "tap1a81968a-79", + "routes": [{ + "netmask": "0.0.0.0", + "network": "0.0.0.0", + "gateway": "172.19.3.254", + }], + "ip_address": "172.19.1.34", "id": "network0" + }, { + "network_id": "private-ipv4", + "type": "ipv4", "netmask": "255.255.255.0", + "link": "tap1a81968a-79", + "routes": [], + "ip_address": "10.0.0.10", "id": "network1" + }], + "links": [ + { + "ethernet_mac_address": "fa:16:3e:ed:9a:59", + "mtu": None, "type": "bridge", "id": + "tap1a81968a-79", + "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f" + }, + ], + }, + 'in_macs': { + 'fa:16:3e:ed:9a:59': 'eth0', + }, + 'out_sysconfig': [ + ('etc/sysconfig/network-scripts/ifcfg-eth0', + """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=none +DEVICE=eth0 +HWADDR=fa:16:3e:ed:9a:59 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), + ('etc/sysconfig/network-scripts/ifcfg-eth0:0', + """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=static +DEFROUTE=yes +DEVICE=eth0:0 +GATEWAY=172.19.3.254 +HWADDR=fa:16:3e:ed:9a:59 +IPADDR=172.19.1.34 +NETMASK=255.255.252.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), + ('etc/sysconfig/network-scripts/ifcfg-eth0:1', + """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=static +DEVICE=eth0:1 +HWADDR=fa:16:3e:ed:9a:59 +IPADDR=10.0.0.10 +NETMASK=255.255.255.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), + ('etc/resolv.conf', + """ +; Created by cloud-init on instance boot automatically, do not edit. +; +nameserver 172.19.0.12 +""".lstrip()), + ('etc/udev/rules.d/70-persistent-net.rules', + "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', + 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))] } ] @@ -222,11 +304,9 @@ NETWORK_CONFIGS = { auto eth99 iface eth99 inet dhcp - post-up ifup eth99:1 - - auto eth99:1 - iface eth99:1 inet static + # control-alias eth99 + iface eth99 inet static address 192.168.21.3/24 dns-nameservers 8.8.8.8 8.8.4.4 dns-search barley.maas sach.maas @@ -264,6 +344,27 @@ NETWORK_CONFIGS = { - wark.maas """), }, + 'v4_and_v6': { + 'expected_eni': textwrap.dedent("""\ + auto lo + iface lo inet loopback + + auto iface0 + iface iface0 inet dhcp + + # control-alias iface0 + iface iface0 inet6 dhcp + """).rstrip(' '), + 'yaml': textwrap.dedent("""\ + version: 1 + config: + - type: 'physical' + name: 'iface0' + subnets: + - {'type': 'dhcp4'} + - {'type': 'dhcp6'} + """).rstrip(' '), + }, 'all': { 'expected_eni': ("""\ auto lo @@ -301,11 +402,9 @@ iface br0 inet static address 192.168.14.2/24 bridge_ports eth3 eth4 bridge_stp off - post-up ifup br0:1 - -auto br0:1 -iface br0:1 inet6 static +# control-alias br0 +iface br0 inet6 static address 2001:1::1/64 auto bond0.200 @@ -322,11 +421,9 @@ iface eth0.101 inet static mtu 1500 vlan-raw-device eth0 vlan_id 101 - post-up ifup eth0.101:1 - -auto eth0.101:1 -iface eth0.101:1 inet static +# control-alias eth0.101 +iface eth0.101 inet static address 192.168.2.10/24 post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true @@ -478,7 +575,7 @@ def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path.side_effect = sys_dev_path -class TestSysConfigRendering(TestCase): +class TestSysConfigRendering(CiTestCase): @mock.patch("cloudinit.net.sys_dev_path") @mock.patch("cloudinit.net.read_sys_net") @@ -486,8 +583,7 @@ class TestSysConfigRendering(TestCase): def test_default_generation(self, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path): - tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, tmp_dir) + tmp_dir = self.tmp_dir() _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path) @@ -518,10 +614,8 @@ USERCTL=no self.assertEqual(expected_content, content) def test_openstack_rendering_samples(self): - tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, tmp_dir) - render_dir = os.path.join(tmp_dir, "render") for os_sample in OS_SAMPLES: + render_dir = self.tmp_dir() ex_input = os_sample['in_data'] ex_mac_addrs = os_sample['in_macs'] network_cfg = openstack.convert_net_json( @@ -535,7 +629,7 @@ USERCTL=no self.assertEqual(expected_content, fh.read()) -class TestEniNetRendering(TestCase): +class TestEniNetRendering(CiTestCase): @mock.patch("cloudinit.net.sys_dev_path") @mock.patch("cloudinit.net.read_sys_net") @@ -543,8 +637,7 @@ class TestEniNetRendering(TestCase): def test_default_generation(self, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path): - tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, tmp_dir) + tmp_dir = self.tmp_dir() _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, mock_sys_dev_path) @@ -576,7 +669,7 @@ iface eth1000 inet dhcp self.assertEqual(expected.lstrip(), contents.lstrip()) -class TestEniNetworkStateToEni(TestCase): +class TestEniNetworkStateToEni(CiTestCase): mycfg = { 'config': [{"type": "physical", "name": "eth0", "mac_address": "c0:d6:9f:2c:e8:80", @@ -607,7 +700,7 @@ class TestEniNetworkStateToEni(TestCase): self.assertNotIn("hwaddress", rendered) -class TestCmdlineConfigParsing(TestCase): +class TestCmdlineConfigParsing(CiTestCase): simple_cfg = { 'config': [{"type": "physical", "name": "eth0", "mac_address": "c0:d6:9f:2c:e8:80", @@ -665,7 +758,7 @@ class TestCmdlineConfigParsing(TestCase): self.assertEqual(found, self.simple_cfg) -class TestCmdlineReadKernelConfig(TempDirTestCase): +class TestCmdlineReadKernelConfig(CiTestCase): macs = { 'eth0': '14:02:ec:42:48:00', 'eno1': '14:02:ec:42:48:01', @@ -673,8 +766,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_read_kernel_cmdline_ip(self): content = {'net-eth0.conf': DHCP_CONTENT_1} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in content.keys()] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo ip=dhcp', mac_addrs=self.macs) exp1 = copy.deepcopy(DHCP_EXPECTED_1) @@ -684,8 +776,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_read_kernel_cmdline_ip6(self): content = {'net6-eno1.conf': DHCP6_CONTENT_1} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in content.keys()] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo ip6=dhcp root=/dev/sda', mac_addrs=self.macs) @@ -701,8 +792,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_read_kernel_cmdline_none(self): # if there is no ip= or ip6= on cmdline, return value should be None content = {'net6-eno1.conf': DHCP6_CONTENT_1} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in content.keys()] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo root=/dev/sda', mac_addrs=self.macs) self.assertEqual(found, None) @@ -710,8 +800,7 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): def test_ip_cmdline_both_ip_ip6(self): content = {'net-eth0.conf': DHCP_CONTENT_1, 'net6-eth0.conf': DHCP6_CONTENT_1.replace('eno1', 'eth0')} - populate_dir(self.tmp, content) - files = [os.path.join(self.tmp, k) for k in sorted(content.keys())] + files = sorted(populate_dir(self.tmp_dir(), content)) found = cmdline.read_kernel_cmdline_config( files=files, cmdline='foo ip=dhcp ip6=dhcp', mac_addrs=self.macs) @@ -725,14 +814,12 @@ class TestCmdlineReadKernelConfig(TempDirTestCase): self.assertEqual(found['config'], expected) -class TestEniRoundTrip(TestCase): - def setUp(self): - super(TestCase, self).setUp() - self.tmp_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.tmp_dir) - +class TestEniRoundTrip(CiTestCase): def _render_and_read(self, network_config=None, state=None, eni_path=None, - links_prefix=None, netrules_path=None): + links_prefix=None, netrules_path=None, dir=None): + if dir is None: + dir = self.tmp_dir() + if network_config: ns = network_state.parse_net_config_data(network_config) elif state: @@ -747,8 +834,8 @@ class TestEniRoundTrip(TestCase): config={'eni_path': eni_path, 'links_path_prefix': links_prefix, 'netrules_path': netrules_path}) - renderer.render_network_state(self.tmp_dir, ns) - return dir2dict(self.tmp_dir) + renderer.render_network_state(dir, ns) + return dir2dict(dir) def testsimple_convert_and_render(self): network_config = eni.convert_eni_data(EXAMPLE_ENI) @@ -771,6 +858,13 @@ class TestEniRoundTrip(TestCase): entry['expected_eni'].splitlines(), files['/etc/network/interfaces'].splitlines()) + def testsimple_render_v4_and_v6(self): + entry = NETWORK_CONFIGS['v4_and_v6'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + self.assertEqual( + entry['expected_eni'].splitlines(), + files['/etc/network/interfaces'].splitlines()) + def test_routes_rendered(self): # as reported in bug 1649652 conf = [ diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py index 55971b5e..991f45a6 100644 --- a/tests/unittests/test_sshutil.py +++ b/tests/unittests/test_sshutil.py @@ -32,6 +32,22 @@ VALID_CONTENT = { "YWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07" "/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw==" ), + 'ecdsa-sha2-nistp256': ( + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMy/WuXq5MF" + "r5hVQ9EEKKUTF7vUaOkgxUh6bNsCs9SFMVslIm1zM/WJYwUv52LdEePjtDYiV4A" + "l2XthJ9/bs7Pc=" + ), + 'ecdsa-sha2-nistp521': ( + "AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABOdNTkh9F" + "McK4hZRLs5LTXBEXwNr0+Yg9uvJYRFcz2ZlnjYX9tM4Z3QQFjqogU4pU+zpKLqZ" + "5VE4Jcnb1T608UywBIdXkSFZT8trGJqBv9nFWGgmTX3KP8kiBbihpuv1cGwglPl" + "Hxs50A42iP0JiT7auGtEAGsu/uMql323GTGb4171Q==" + ), + 'ecdsa-sha2-nistp384': ( + "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBAnoqFU9Gnl" + "LcsEuCJnobs/c6whzvjCgouaOO61kgXNtIxyF4Wkutg6xaGYgBBt/phb7a2TurI" + "bcIBuzJ/mP22UyUAbNnBfStAEBmYbrTf1EfiMCYUAr1XnL0UdYmZ8HFg==" + ), } TEST_OPTIONS = ( @@ -44,7 +60,13 @@ class TestAuthKeyLineParser(test_helpers.TestCase): def test_simple_parse(self): # test key line with common 3 fields (keytype, base64, comment) parser = ssh_util.AuthKeyLineParser() - for ktype in ['rsa', 'ecdsa', 'dsa']: + ecdsa_types = [ + 'ecdsa-sha2-nistp256', + 'ecdsa-sha2-nistp384', + 'ecdsa-sha2-nistp521', + ] + + for ktype in ['rsa', 'ecdsa', 'dsa'] + ecdsa_types: content = VALID_CONTENT[ktype] comment = 'user-%s@host' % ktype line = ' '.join((ktype, content, comment,)) diff --git a/tools/Z99-cloud-locale-test.sh b/tools/Z99-cloud-locale-test.sh index 5912bae2..4978d87e 100755..100644 --- a/tools/Z99-cloud-locale-test.sh +++ b/tools/Z99-cloud-locale-test.sh @@ -11,90 +11,90 @@ # of how to fix them. locale_warn() { - local bad_names="" bad_lcs="" key="" val="" var="" vars="" bad_kv="" - local w1 w2 w3 w4 remain + local bad_names="" bad_lcs="" key="" val="" var="" vars="" bad_kv="" + local w1 w2 w3 w4 remain - # if shell is zsh, act like sh only for this function (-L). - # The behavior change will not permenently affect user's shell. - [ "${ZSH_NAME+zsh}" = "zsh" ] && emulate -L sh + # if shell is zsh, act like sh only for this function (-L). + # The behavior change will not permenently affect user's shell. + [ "${ZSH_NAME+zsh}" = "zsh" ] && emulate -L sh - # locale is expected to output either: - # VARIABLE= - # VARIABLE="value" - # locale: Cannot set LC_SOMETHING to default locale - while read -r w1 w2 w3 w4 remain; do - case "$w1" in - locale:) bad_names="${bad_names} ${w4}";; - *) - key=${w1%%=*} - val=${w1#*=} - val=${val#\"} - val=${val%\"} - vars="${vars} $key=$val";; - esac - done - for bad in $bad_names; do - for var in ${vars}; do - [ "${bad}" = "${var%=*}" ] || continue - val=${var#*=} - [ "${bad_lcs#* ${val}}" = "${bad_lcs}" ] && - bad_lcs="${bad_lcs} ${val}" - bad_kv="${bad_kv} $bad=$val" - break - done - done - bad_lcs=${bad_lcs# } - bad_kv=${bad_kv# } - [ -n "$bad_lcs" ] || return 0 + # locale is expected to output either: + # VARIABLE= + # VARIABLE="value" + # locale: Cannot set LC_SOMETHING to default locale + while read -r w1 w2 w3 w4 remain; do + case "$w1" in + locale:) bad_names="${bad_names} ${w4}";; + *) + key=${w1%%=*} + val=${w1#*=} + val=${val#\"} + val=${val%\"} + vars="${vars} $key=$val";; + esac + done + for bad in $bad_names; do + for var in ${vars}; do + [ "${bad}" = "${var%=*}" ] || continue + val=${var#*=} + [ "${bad_lcs#* ${val}}" = "${bad_lcs}" ] && + bad_lcs="${bad_lcs} ${val}" + bad_kv="${bad_kv} $bad=$val" + break + done + done + bad_lcs=${bad_lcs# } + bad_kv=${bad_kv# } + [ -n "$bad_lcs" ] || return 0 - printf "_____________________________________________________________________\n" - printf "WARNING! Your environment specifies an invalid locale.\n" - printf " The unknown environment variables are:\n %s\n" "$bad_kv" - printf " This can affect your user experience significantly, including the\n" - printf " ability to manage packages. You may install the locales by running:\n\n" + printf "_____________________________________________________________________\n" + printf "WARNING! Your environment specifies an invalid locale.\n" + printf " The unknown environment variables are:\n %s\n" "$bad_kv" + printf " This can affect your user experience significantly, including the\n" + printf " ability to manage packages. You may install the locales by running:\n\n" - local bad invalid="" to_gen="" sfile="/usr/share/i18n/SUPPORTED" - local pkgs="" - if [ -e "$sfile" ]; then - for bad in ${bad_lcs}; do - grep -q -i "${bad}" "$sfile" && - to_gen="${to_gen} ${bad}" || - invalid="${invalid} ${bad}" - done - else - printf " sudo apt-get install locales\n" - to_gen=$bad_lcs - fi - to_gen=${to_gen# } + local bad invalid="" to_gen="" sfile="/usr/share/i18n/SUPPORTED" + local pkgs="" + if [ -e "$sfile" ]; then + for bad in ${bad_lcs}; do + grep -q -i "${bad}" "$sfile" && + to_gen="${to_gen} ${bad}" || + invalid="${invalid} ${bad}" + done + else + printf " sudo apt-get install locales\n" + to_gen=$bad_lcs + fi + to_gen=${to_gen# } - local pkgs="" - for bad in ${to_gen}; do - pkgs="${pkgs} language-pack-${bad%%_*}" - done - pkgs=${pkgs# } + local pkgs="" + for bad in ${to_gen}; do + pkgs="${pkgs} language-pack-${bad%%_*}" + done + pkgs=${pkgs# } - if [ -n "${pkgs}" ]; then - printf " sudo apt-get install ${pkgs# }\n" - printf " or\n" - printf " sudo locale-gen ${to_gen# }\n" - printf "\n" - fi - for bad in ${invalid}; do - printf "WARNING: '${bad}' is an invalid locale\n" - done + if [ -n "${pkgs}" ]; then + printf " sudo apt-get install ${pkgs# }\n" + printf " or\n" + printf " sudo locale-gen ${to_gen# }\n" + printf "\n" + fi + for bad in ${invalid}; do + printf "WARNING: '${bad}' is an invalid locale\n" + done - printf "To see all available language packs, run:\n" - printf " apt-cache search \"^language-pack-[a-z][a-z]$\"\n" - printf "To disable this message for all users, run:\n" - printf " sudo touch /var/lib/cloud/instance/locale-check.skip\n" - printf "_____________________________________________________________________\n\n" + printf "To see all available language packs, run:\n" + printf " apt-cache search \"^language-pack-[a-z][a-z]$\"\n" + printf "To disable this message for all users, run:\n" + printf " sudo touch /var/lib/cloud/instance/locale-check.skip\n" + printf "_____________________________________________________________________\n\n" - # only show the message once - : > ~/.cloud-locale-test.skip 2>/dev/null || : + # only show the message once + : > ~/.cloud-locale-test.skip 2>/dev/null || : } [ -f ~/.cloud-locale-test.skip -o -f /var/lib/cloud/instance/locale-check.skip ] || - locale 2>&1 | locale_warn + locale 2>&1 | locale_warn unset locale_warn -# vi: ts=4 noexpandtab +# vi: ts=4 expandtab diff --git a/tools/Z99-cloudinit-warnings.sh b/tools/Z99-cloudinit-warnings.sh new file mode 100644 index 00000000..b237786b --- /dev/null +++ b/tools/Z99-cloudinit-warnings.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# This file is part of cloud-init. See LICENSE file for license information. + +# Purpose: show user warnings on login. + +cloud_init_warnings() { + local skipf="" warning="" idir="/var/lib/cloud/instance" n=0 + local warndir="$idir/warnings" + local ufile="$HOME/.cloud-warnings.skip" sfile="$warndir/.skip" + [ -d "$warndir" ] || return 0 + [ ! -f "$ufile" ] || return 0 + [ ! -f "$skipf" ] || return 0 + + for warning in "$warndir"/*; do + [ -f "$warning" ] || continue + cat "$warning" + n=$((n+1)) + done + [ $n -eq 0 ] && return 0 + echo "" + echo "Disable the warnings above by:" + echo " touch $ufile" + echo "or" + echo " touch $sfile" +} + +cloud_init_warnings 1>&2 +unset cloud_init_warnings + +# vi: syntax=sh ts=4 expandtab diff --git a/tools/ds-identify b/tools/ds-identify new file mode 100755 index 00000000..d7b2a0b2 --- /dev/null +++ b/tools/ds-identify @@ -0,0 +1,1240 @@ +#!/bin/sh +# +# ds-identify is configured via /etc/cloud/ds-identify.cfg +# or on the kernel command line. It takes primarily 2 inputs: +# datasource: can specify the datasource that should be used. +# kernel command line option: ci.datasource=<dsname> +# +# policy: a string that indicates how ds-identify should operate. +# kernel command line option: ci.di.policy=<policy> +# default setting is: +# search,found=all,maybe=all,notfound=disable +# +# report: write config to /run/cloud-init/cloud.cfg, but +# namespaced under 'di_report'. Thus cloud-init can still see +# the result, but has no affect. +# enable: do nothing +# ds-identify writes no config and just exits success +# the caller (cloud-init-generator) then enables cloud-init to run +# just without any aid from ds-identify. +# disable: disable cloud-init +# +# [report,]found=value,maybe=value,notfound=value +# found: (default=first) +# first: use the first found do no further checking +# all: enable all DS_FOUND +# +# maybe: (default=all) +# if nothing returned 'found', then how to handle maybe. +# no network sources are allowed to return 'maybe'. +# all: enable all DS_MAYBE +# none: ignore any DS_MAYBE +# +# notfound: (default=disabled) +# disabled: disable cloud-init +# enabled: enable cloud-init +# +# ci.datasource.ec2.strict_id: (true|false|warn[,0-9]) +# if ec2 datasource does not strictly match, +# return not_found if true +# return maybe if false or warn*. +# + +set -u +set -f +UNAVAILABLE="unavailable" +CR=" +" +ERROR="error" +DI_ENABLED="enabled" +DI_DISABLED="disabled" + +DI_DEBUG_LEVEL="${DEBUG_LEVEL:-1}" + +PATH_ROOT=${PATH_ROOT:-""} +PATH_RUN=${PATH_RUN:-"${PATH_ROOT}/run"} +PATH_SYS_CLASS_DMI_ID=${PATH_SYS_CLASS_DMI_ID:-${PATH_ROOT}/sys/class/dmi/id} +PATH_SYS_HYPERVISOR=${PATH_SYS_HYPERVISOR:-${PATH_ROOT}/sys/hypervisor} +PATH_SYS_CLASS_BLOCK=${PATH_SYS_CLASS_BLOCK:-${PATH_ROOT}/sys/class/block} +PATH_DEV_DISK="${PATH_DEV_DISK:-${PATH_ROOT}/dev/disk}" +PATH_VAR_LIB_CLOUD="${PATH_VAR_LIB_CLOUD:-${PATH_ROOT}/var/lib/cloud}" +PATH_DI_CONFIG="${PATH_DI_CONFIG:-${PATH_ROOT}/etc/cloud/ds-identify.cfg}" +PATH_PROC_CMDLINE="${PATH_PROC_CMDLINE:-${PATH_ROOT}/proc/cmdline}" +PATH_PROC_1_CMDLINE="${PATH_PROC_1_CMDLINE:-${PATH_ROOT}/proc/1/cmdline}" +PATH_PROC_1_ENVIRON="${PATH_PROC_1_ENVIRON:-${PATH_ROOT}/proc/1/environ}" +PATH_PROC_UPTIME=${PATH_PROC_UPTIME:-${PATH_ROOT}/proc/uptime} +PATH_CLOUD_CONFD="${PATH_CLOUD_CONFD:-${PATH_ROOT}/etc/cloud}" +PATH_RUN_CI="${PATH_RUN_CI:-${PATH_RUN}/cloud-init}" +PATH_RUN_CI_CFG=${PATH_RUN_CI_CFG:-${PATH_RUN_CI}/cloud.cfg} +PATH_RUN_DI_RESULT=${PATH_RUN_DI_RESULT:-${PATH_RUN_CI}/.ds-identify.result} + +DI_LOG="${DI_LOG:-${PATH_RUN_CI}/ds-identify.log}" +_DI_LOGGED="" + +# set DI_MAIN='noop' in environment to source this file with no main called. +DI_MAIN=${DI_MAIN:-main} + +DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}" +DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}" +DI_DMI_PRODUCT_NAME="" +DI_DMI_SYS_VENDOR="" +DI_DMI_PRODUCT_SERIAL="" +DI_DMI_PRODUCT_UUID="" +DI_FS_LABELS="" +DI_KERNEL_CMDLINE="" +DI_VIRT="" +DI_PID_1_PLATFORM="" + +DI_UNAME_KERNEL_NAME="" +DI_UNAME_KERNEL_RELEASE="" +DI_UNAME_KERNEL_VERSION="" +DI_UNAME_MACHINE="" +DI_UNAME_NODENAME="" +DI_UNAME_OPERATING_SYSTEM="" +DI_UNAME_CMD_OUT="" + +DS_FOUND=0 +DS_NOT_FOUND=1 +DS_MAYBE=2 + +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 Ec2 OpenNebula OpenStack OVF SmartOS" +DI_DSLIST="" +DI_MODE="" +DI_REPORT="" +DI_ON_FOUND="" +DI_ON_MAYBE="" +DI_ON_NOTFOUND="" + +DI_EC2_STRICT_ID_DEFAULT="true" + +error() { + set -- "ERROR:" "$@"; + debug 0 "$@" + stderr "$@" +} +warn() { + set -- "WARN:" "$@" + debug 0 "$@" + stderr "$@" +} + +stderr() { echo "$@" 1>&2; } + +debug() { + local lvl="$1" + shift + [ "$lvl" -gt "${DI_DEBUG_LEVEL}" ] && return + + if [ "$_DI_LOGGED" != "$DI_LOG" ]; then + # first time here, open file descriptor for append + case "$DI_LOG" in + stderr) :;; + ?*/*) + if [ ! -d "${DI_LOG%/*}" ]; then + mkdir -p "${DI_LOG%/*}" || { + stderr "ERROR:" "cannot write to $DI_LOG" + DI_LOG="stderr" + } + fi + esac + if [ "$DI_LOG" = "stderr" ]; then + exec 3>&2 + else + ( exec 3>>"$DI_LOG" ) && exec 3>>"$DI_LOG" || { + stderr "ERROR: failed writing to $DI_LOG. logging to stderr."; + exec 3>&2 + DI_LOG="stderr" + } + fi + _DI_LOGGED="$DI_LOG" + fi + echo "$@" 1>&3 +} + +get_dmi_field() { + local path="${PATH_SYS_CLASS_DMI_ID}/$1" + if [ ! -f "$path" ] || [ ! -r "$path" ]; then + _RET="$UNAVAILABLE" + return + fi + read _RET < "${path}" || _RET="$ERROR" +} + +block_dev_with_label() { + local p="${PATH_DEV_DISK}/by-label/$1" + [ -b "$p" ] || return 1 + _RET=$p + return 0 +} + +read_fs_labels() { + cached "${DI_FS_LABELS}" && return 0 + # do not rely on links in /dev/disk which might not be present yet. + # note that older blkid versions do not report DEVNAME in 'export' output. + local out="" ret=0 oifs="$IFS" line="" delim="," + local labels="" + if is_container; then + # blkid will in a container, or at least currently in lxd + # not provide useful information. + DI_FS_LABELS="$UNAVAILABLE:container" + else + out=$(blkid -c /dev/null -o export) || { + ret=$? + error "failed running [$ret]: blkid -c /dev/null -o export" + return $ret + } + IFS="$CR" + set -- $out + IFS="$oifs" + for line in "$@"; do + case "${line}" in + LABEL=*) labels="${labels}${line#LABEL=}${delim}";; + esac + done + DI_FS_LABELS="${labels%${delim}}" + fi +} + +cached() { + [ -n "$1" ] && _RET="$1" && return || return 1 +} + + +has_cdrom() { + [ -e "${PATH_ROOT}/dev/cdrom" ] +} + +read_virt() { + cached "$DI_VIRT" && return 0 + local out="" r="" virt="${UNAVAILABLE}" + if [ -d /run/systemd ]; then + out=$(systemd-detect-virt 2>&1) + r=$? + if [ $r -eq 0 ] || { [ $r -ne 0 ] && [ "$out" = "none" ]; }; then + virt="$out" + fi + fi + DI_VIRT=$virt +} + +is_container() { + case "${DI_VIRT}" in + lxc|lxc-libvirt|systemd-nspawn|docker|rkt) return 0;; + *) return 1;; + esac +} + +read_kernel_cmdline() { + cached "${DI_KERNEL_CMDLINE}" && return + local cmdline="" fpath="${PATH_PROC_CMDLINE}" + if is_container; then + local p1path="${PATH_PROC_1_CMDLINE}" x="" + cmdline="${UNAVAILABLE}:container" + if [ -f "$p1path" ] && x=$(tr '\0' ' ' < "$p1path"); then + cmdline=$x + fi + elif [ -f "$fpath" ]; then + read cmdline <"$fpath" + else + cmdline="${UNAVAILABLE}:no-cmdline" + fi + DI_KERNEL_CMDLINE="$cmdline" +} + +read_dmi_sys_vendor() { + cached "${DI_DMI_SYS_VENDOR}" && return + get_dmi_field sys_vendor + DI_DMI_SYS_VENDOR="$_RET" +} + +read_dmi_product_name() { + cached "${DI_DMI_PRODUCT_NAME}" && return + get_dmi_field product_name + DI_DMI_PRODUCT_NAME="$_RET" +} + +read_dmi_product_uuid() { + cached "${DI_DMI_PRODUCT_UUID}" && return + get_dmi_field product_uuid + DI_DMI_PRODUCT_UUID="$_RET" +} + +read_dmi_product_serial() { + cached "${DI_DMI_PRODUCT_SERIAL}" && return + get_dmi_field product_serial + DI_DMI_PRODUCT_SERIAL="$_RET" +} + +read_uname_info() { + # run uname, and parse output. + # uname is tricky to parse as it outputs always in a given order + # independent of option order. kernel-version is known to have spaces. + # 1 -s kernel-name + # 2 -n nodename + # 3 -r kernel-release + # 4.. -v kernel-version(whitespace) + # N-2 -m machine + # N-1 -o operating-system + cached "${DI_UNAME_CMD_OUT}" && return + local out="${1:-}" ret=0 buf="" + if [ -z "$out" ]; then + out=$(uname -snrvmo) || { + ret=$? + error "failed reading uname with 'uname -snrvmo'" + return $ret + } + fi + set -- $out + DI_UNAME_KERNEL_NAME="$1" + DI_UNAME_NODENAME="$2" + DI_UNAME_KERNEL_RELEASE="$3" + shift 3 + while [ $# -gt 2 ]; do + buf="$buf $1" + shift + done + DI_UNAME_KERNEL_VERSION="${buf# }" + DI_UNAME_MACHINE="$1" + DI_UNAME_OPERATING_SYSTEM="$2" + DI_UNAME_CMD_OUT="$out" + return 0 +} + +parse_yaml_array() { + # parse a yaml single line array value ([1,2,3], not key: [1,2,3]). + # supported with or without leading and closing brackets + # ['1'] or [1] + # '1', '2' + local val="$1" oifs="$IFS" ret="" tok="" + val=${val#[} + val=${val%]} + IFS=","; set -- $val; IFS="$oifs" + for tok in "$@"; do + trim "$tok" + unquote "$_RET" + ret="${ret} $_RET" + done + _RET="${ret# }" +} + +read_datasource_list() { + cached "$DI_DSLIST" && return + local dslist="" + # if DI_DSNAME is set directly, then avoid parsing config. + if [ -n "${DI_DSNAME}" ]; then + dslist="${DI_DSNAME}" + fi + + # LP: #1582323. cc:{'datasource_list': ['name']} + # more generically cc:<yaml>[end_cc] + local cb="]" ob="[" + case "$DI_KERNEL_CMDLINE" in + *cc:*datasource_list*) + t=${DI_KERNEL_CMDLINE##*datasource_list} + t=${t%%$cb*} + t=${t##*$ob} + parse_yaml_array "$t" + dslist=${_RET} + ;; + esac + if [ -z "$dslist" ] && check_config datasource_list; then + debug 1 "$_RET_fname set datasource_list: $_RET" + parse_yaml_array "$_RET" + dslist=${_RET} + fi + if [ -z "$dslist" ]; then + dslist=${DI_DSLIST_DEFAULT} + debug 1 "no datasource_list found, using default:" $dslist + fi + DI_DSLIST=$dslist + return 0 +} + +read_pid1_platform() { + local oifs="$IFS" out="" tok="" key="" val="" platform="${UNAVAILABLE}" + cached "${DI_PID_1_PLATFORM}" && return + [ -r "${PATH_PROC_1_ENVIRON}" ] || return + out=$(tr '\0' '\n' <"${PATH_PROC_1_ENVIRON}") + IFS="$CR"; set -- $out; IFS="$oifs" + for tok in "$@"; do + key=${tok%%=*} + [ "$key" != "$tok" ] || continue + val=${tok#*=} + [ "$key" = "platform" ] && platform="$val" && break + done + DI_PID_1_PLATFORM="$platform" +} + +dmi_product_name_matches() { + is_container && return 1 + case "${DI_DMI_PRODUCT_NAME}" in + $1) return 0;; + esac + return 1 +} + +dmi_product_name_is() { + is_container && return 1 + [ "${DI_DMI_PRODUCT_NAME}" = "$1" ] +} + +dmi_sys_vendor_is() { + is_container && return 1 + [ "${DI_DMI_SYS_VENDOR}" = "$1" ] +} + +has_fs_with_label() { + local label="$1" + case ",${DI_FS_LABELS}," in + *,$label,*) return 0;; + esac + return 1 +} + +nocase_equal() { + # nocase_equal(a, b) + # return 0 if case insenstive comparision a.lower() == b.lower() + # different lengths + [ "${#1}" = "${#2}" ] || return 1 + # case sensitive equal + [ "$1" = "$2" ] && return 0 + + local delim="-delim-" + out=$(echo "$1${delim}$2" | tr A-Z a-z) + [ "${out#*${delim}}" = "${out%${delim}*}" ] +} + +check_seed_dir() { + # check_seed_dir(name, [required]) + # check the seed dir /var/lib/cloud/seed/<name> for 'required' + # required defaults to 'meta-data' + local name="$1" + local dir="${PATH_VAR_LIB_CLOUD}/seed/$name" + [ -d "$dir" ] || return 1 + shift + if [ $# -eq 0 ]; then + set -- meta-data + fi + local f="" + for f in "$@"; do + [ -f "$dir/$f" ] || return 1 + done + return 0 +} + +probe_floppy() { + cached "${STATE_FLOPPY_PROBED}" && return "${STATE_FLOPPY_PROBED}" + local fpath=/dev/floppy + + [ -b "$fpath" ] || + { STATE_FLOPPY_PROBED=1; return 1; } + + modprobe --use-blacklist floppy >/dev/null 2>&1 || + { STATE_FLOPPY_PROBED=1; return 1; } + + udevadm settle "--exit-if-exists=$fpath" || + { STATE_FLOPPY_PROBED=1; return 1; } + + [ -b "$fpath" ] + STATE_FLOPPY_PROBED=$? + return "${STATE_FLOPPY_PROBED}" +} + + +dscheck_CloudStack() { + is_container && return ${DS_NOT_FOUND} + dmi_product_name_matches "CloudStack*" && return $DS_FOUND + return $DS_NOT_FOUND +} + +dscheck_CloudSigma() { + # http://paste.ubuntu.com/23624795/ + dmi_product_name_is "CloudSigma" && return $DS_FOUND + return $DS_NOT_FOUND +} + +check_config() { + # somewhat hackily read config for 'key' in files matching 'files' + # currently does not respect any hierarchy. + local key="$1" files="" bp="${PATH_CLOUD_CONFD}/cloud.cfg" + if [ $# -eq 1 ]; then + files="$bp ${bp}.d/*.cfg" + else + files="$*" + fi + shift + set +f; set -- $files; set +f; + if [ "$1" = "$files" -a ! -f "$1" ]; then + return 1 + fi + local fname="" line="" ret="" found=0 found_fn="" + for fname in "$@"; do + [ -f "$fname" ] || continue + while read line; do + line=${line%%#*} + case "$line" in + $key:\ *|$key:) + ret=${line#*:}; + ret=${ret# }; + found=$((found+1)) + found_fn="$fname";; + esac + done <"$fname" + done + if [ $found -ne 0 ]; then + _RET="$ret" + _RET_fname="$found_fn" + return 0 + fi + return 1 +} + +dscheck_MAAS() { + is_container && return "${DS_NOT_FOUND}" + # heuristic check for ephemeral boot environment + # for maas that do not set 'ci.dsname=' in the ephemeral environment + # these have iscsi root and cloud-config-url on the cmdline. + local maasiqn="iqn.2004-05.com.ubuntu:maas" + case "${DI_KERNEL_CMDLINE}" in + *cloud-config-url=*${maasiqn}*|*${maasiqn}*cloud-config-url=*) + return ${DS_FOUND} + ;; + esac + + # check config files written by maas for installed system. + local confd="${PATH_CLOUD_CONFD}" + local fnmatch="$confd/*maas*.cfg $confd/*kernel_cmdline*.cfg" + if check_config "MAAS" "$fnmatch"; then + return "${DS_FOUND}" + fi + return ${DS_NOT_FOUND} +} + +dscheck_NoCloud() { + local fslabel="cidata" d="" + case " ${DI_KERNEL_CMDLINE} " in + *\ ds=nocloud*) return ${DS_FOUND};; + esac + for d in nocloud nocloud-net; do + check_seed_dir "$d" meta-data user-data && return ${DS_FOUND} + done + if has_fs_with_label "${fslabel}"; then + return ${DS_FOUND} + fi + return ${DS_NOT_FOUND} +} + +check_configdrive_v2() { + if has_fs_with_label "config-2"; then + return ${DS_FOUND} + fi + return ${DS_NOT_FOUND} +} + +check_configdrive_v1() { + # FIXME: this has to check any file system that is vfat... + # for now, just return not found. + return ${DS_NOT_FOUND} +} + +dscheck_ConfigDrive() { + local ret="" + check_configdrive_v2 + ret=$? + [ $DS_FOUND -eq $ret ] && return $ret + + check_configdrive_v1 +} + +dscheck_DigitalOcean() { + dmi_sys_vendor_is DigitalOcean && return ${DS_FOUND} + return ${DS_NOT_FOUND} +} + +dscheck_OpenNebula() { + check_seed_dir opennebula && return ${DS_FOUND} + has_fs_with_label "CONTEXT" && return ${DS_FOUND} + return ${DS_NOT_FOUND} +} + +ovf_vmware_guest_customization() { + # vmware guest customization + + # virt provider must be vmware + [ "${DI_VIRT}" = "vmware" ] || return 1 + + # we have to have the plugin to do vmware customization + local found="" pkg="" pre="/usr/lib" + for pkg in vmware-tools open-vm-tools; do + if [ -f "$pre/$pkg/plugins/vmsvc/libdeployPkgPlugin.so" ]; then + found="$pkg"; break; + fi + done + [ -n "$found" ] || return 1 + + # vmware customization is disabled by default + # (disable_vmware_customization=true). If it is set to false, then + # user has requested customization. + local key="disable_vmware_customization" + local match="" bp="${PATH_CLOUD_CONFD}/cloud.cfg" + match="$bp $bp.d/*[Oo][Vv][Ff]*.cfg" + if check_config "$key" "$match"; then + debug 2 "${_RET_fname} set $key to $_RET" + case "$_RET" in + 0|false|False) return 0;; + *) return 1;; + esac + fi + + return 1 +} + +dscheck_OVF() { + local p="" + check_seed_dir ovf ovf-env.xml && return "${DS_FOUND}" + + if ovf_vmware_guest_customization; then + return ${DS_FOUND} + fi + + has_cdrom || return ${DS_NOT_FOUND} + + # FIXME: currently just return maybe if there is a cdrom + # ovf iso9660 transport does not specify an fs label. + # better would be to check if + return ${DS_MAYBE} +} + +dscheck_Azure() { + # http://paste.ubuntu.com/23630873/ + # $ grep /sr0 /run/blkid/blkid.tab + # <device DEVNO="0x0b00" TIME="1481737655.543841" + # UUID="112D211272645f72" LABEL="rd_rdfe_stable.161212-1209" + # TYPE="udf">/dev/sr0</device> + # + check_seed_dir azure ovf-env.xml && return ${DS_FOUND} + + [ "${DI_VIRT}" = "microsoft" ] || return ${DS_NOT_FOUND} + + has_fs_with_label "rd_rdfe_*" && return ${DS_FOUND} + + return ${DS_NOT_FOUND} +} + +dscheck_Bigstep() { + # bigstep is activated by presense of seed file 'url' + check_seed_dir "bigstep" url && return ${DS_FOUND} + return ${DS_NOT_FOUND} +} + +ec2_read_strict_setting() { + # the 'strict_id' setting for Ec2 controls behavior when + # the platform does not identify itself directly as Ec2. + # order of precedence is: + # 1. builtin setting here cloud-init/ds-identify builtin + # 2. ds-identify config + # 3. system config (/etc/cloud/cloud.cfg.d/*Ec2*.cfg) + # 4. kernel command line (undocumented) + # 5. user-data or vendor-data (not available here) + local default="$1" key="ci.datasource.ec2.strict_id" val="" + + # 4. kernel command line + case " ${DI_KERNEL_CMDLINE} " in + *\ $key=*\ ) + val=${DI_KERNEL_CMDLINE##*$key=} + val=${val%% *}; + _RET=${val:-$default} + return 0 + esac + + # 3. look for the key 'strict_id' (datasource/Ec2/strict_id) + local match="" bp="${PATH_CLOUD_CONFD}/cloud.cfg" + match="$bp $bp.d/*[Ee][Cc]2*.cfg" + if check_config strict_id "$match"; then + debug 2 "${_RET_fname} set strict_id to $_RET" + return 0 + fi + + # 2. ds-identify config (datasource.ec2.strict) + local config="${PATH_DI_CONFIG}" + if [ -f "$config" ]; then + if _read_config "$key" < "$config"; then + _RET=${_RET:-$default} + return 0 + fi + fi + + # 1. Default + _RET=$default + return 0 +} + +ec2_identify_platform() { + local default="$1" + local serial="${DI_DMI_PRODUCT_SERIAL}" + + # brightbox https://bugs.launchpad.net/cloud-init/+bug/1661693 + case "$serial" in + *brightbox.com) _RET="Brightbox"; return 0;; + esac + + # AWS http://docs.aws.amazon.com/AWSEC2/ + # latest/UserGuide/identify_ec2_instances.html + local uuid="" hvuuid="$PATH_ROOT/sys/hypervisor/uuid" + # if the (basically) xen specific /sys/hypervisor/uuid starts with 'ec2' + if [ -r "$hvuuid" ] && read uuid < "$hvuuid" && + [ "${uuid#ec2}" != "$uuid" ]; then + _RET="AWS" + return 0 + fi + + # product uuid and product serial start with case insensitive + local uuid="${DI_DMI_PRODUCT_UUID}" + case "$uuid:$serial" in + [Ee][Cc]2*:[Ee][Cc]2) + # both start with ec2, now check for case insenstive equal + nocase_equal "$uuid" "$serial" && + { _RET="AWS"; return 0; };; + esac + + _RET="$default" + return 0; +} + +dscheck_Ec2() { + check_seed_dir "ec2" meta-data user-data && return ${DS_FOUND} + is_container && return ${DS_NOT_FOUND} + + local unknown="Unknown" platform="" + if ec2_identify_platform "$unknown"; then + platform="$_RET" + else + warn "Failed to identify ec2 platform. Using '$unknown'." + platform=$unknown + fi + + debug 1 "ec2 platform is '$platform'." + if [ "$platform" != "$unknown" ]; then + return $DS_FOUND + fi + + local default="${DI_EC2_STRICT_ID_DEFAULT}" + if ec2_read_strict_setting "$default"; then + strict="$_RET" + else + debug 1 "ec2_read_strict returned non-zero: $?. using '$default'." + strict="$default" + fi + + local key="datasource/Ec2/strict_id" + case "$strict" in + true|false|warn|warn,[0-9]*) :;; + *) + warn "$key was set to invalid '$strict'. using '$default'" + strict="$default";; + esac + + _RET_excfg="datasource: {Ec2: {strict_id: \"$strict\"}}" + if [ "$strict" = "true" ]; then + return $DS_NOT_FOUND + else + return $DS_MAYBE + fi +} + +dscheck_GCE() { + if dmi_product_name_is "Google Compute Engine"; then + return ${DS_FOUND} + fi + return ${DS_NOT_FOUND} +} + +dscheck_OpenStack() { + # the openstack metadata http service + + # if there is a config drive, then do not check metadata + # FIXME: if config drive not in the search list, then we should not + # do this check. + check_configdrive_v2 + if [ $? -eq ${DS_FOUND} ]; then + return ${DS_NOT_FOUND} + fi + if dmi_product_name_is "OpenStack Nova"; then + return ${DS_FOUND} + fi + if [ "${DI_PID_1_PLATFORM}" = "OpenStack Nova" ]; then + return ${DS_FOUND} + fi + + return ${DS_NOT_FOUND} +} + +dscheck_AliYun() { + # aliyun is not enabled by default (LP: #1638931) + # so if we are here, it is because the datasource_list was + # set to include it. Thus, 'maybe'. + return $DS_MAYBE +} + +dscheck_AltCloud() { + # ctype: either the dmi product name, or contents of + # /etc/sysconfig/cloud-info + # if ctype == "vsphere" + # device = device with label 'CDROM' + # elif ctype == "rhev" + # device = /dev/floppy + # then, filesystem on that device must have + # user-data.txt or deltacloud-user-data.txt + local ctype="" dev="" + local match_rhev="[Rr][Hh][Ee][Vv]" + local match_vsphere="[Vv][Ss][Pp][Hh][Ee][Rr][Ee]" + local cinfo="${PATH_ROOT}/etc/sysconfig/cloud-info" + if [ -f "$cinfo" ]; then + read ctype < "$cinfo" + else + ctype="${DI_DMI_PRODUCT_NAME}" + fi + case "$ctype" in + ${match_rhev}) + probe_floppy || return ${DS_NOT_FOUND} + dev="/dev/floppy" + ;; + ${match_vsphere}) + block_dev_with_label CDROM || return ${DS_NOT_FOUND} + dev="$_RET" + ;; + *) return ${DS_NOT_FOUND};; + esac + + # FIXME: need to check $dev for user-data.txt or deltacloud-user-data.txt + : "$dev" + return $DS_MAYBE +} + +dscheck_SmartOS() { + # joyent cloud has two virt types: kvm and container + # on kvm, product name on joyent public cloud shows 'SmartDC HVM' + # on the container platform, uname's version has: BrandZ virtual linux + local smartdc_kver="BrandZ virtual linux" + dmi_product_name_matches "SmartDC*" && return $DS_FOUND + if [ "${DI_UNAME_KERNEL_VERSION}" = "${smartdc_kver}" ] && + [ "${DI_VIRT}" = "container-other" ]; then + return ${DS_FOUND} + fi + return ${DS_NOT_FOUND} +} + +dscheck_None() { + return ${DS_NOT_FOUND} +} + +collect_info() { + read_virt + read_pid1_platform + read_kernel_cmdline + read_uname_info + read_config + read_datasource_list + read_dmi_sys_vendor + read_dmi_product_name + read_dmi_product_serial + read_dmi_product_uuid + read_fs_labels +} + +print_info() { + collect_info + _print_info +} + +_print_info() { + local n="" v="" vars="" + vars="DMI_PRODUCT_NAME DMI_SYS_VENDOR DMI_PRODUCT_SERIAL" + vars="$vars DMI_PRODUCT_UUID PID_1_PLATFORM" + vars="$vars FS_LABELS KERNEL_CMDLINE VIRT" + vars="$vars UNAME_KERNEL_NAME UNAME_KERNEL_RELEASE UNAME_KERNEL_VERSION" + vars="$vars UNAME_MACHINE UNAME_NODENAME UNAME_OPERATING_SYSTEM" + vars="$vars DSNAME DSLIST" + vars="$vars MODE REPORT ON_FOUND ON_MAYBE ON_NOTFOUND" + for v in ${vars}; do + eval n='${DI_'"$v"'}' + echo "$v=$n" + done + echo "pid=$$ ppid=$PPID" + is_container && echo "is_container=true" || echo "is_container=false" +} + +write_result() { + local runcfg="${PATH_RUN_CI_CFG}" ret="" line="" pre="" + { + if [ "$DI_REPORT" = "true" ]; then + echo "di_report:" + pre=" " + fi + for line in "$@"; do + echo "${pre}$line"; + done + } > "$runcfg" + ret=$? + [ $ret -eq 0 ] || { + error "failed to write to ${runcfg}" + return $ret + } + return 0 +} + +record_notfound() { + # in report mode, report nothing was found. + # if not report mode: only report the negative result. + # reporting an empty list would mean cloud-init would not search + # any datasources. + if [ "$DI_REPORT" = "true" ]; then + found -- + else + local msg="# reporting not found result. notfound=${DI_ON_NOTFOUND}." + local DI_REPORT="true" + found -- "$msg" + fi +} + +found() { + # found(ds1, [ds2 ...], [-- [extra lines]]) + local list="" ds="" + while [ $# -ne 0 ]; do + if [ "$1" = "--" ]; then + shift + break + fi + list="${list:+${list}, }$1" + shift + done + if [ $# -eq 1 ] && [ -z "$1" ]; then + # do not pass an empty line through. + shift + fi + # always write the None datasource last. + list="${list:+${list}, }None" + write_result "datasource_list: [ $list ]" "$@" + return +} + +trim() { + set -- $* + _RET="$*" +} + +unquote() { + # remove quotes from quoted value + local quote='"' tick="'" + local val="$1" + case "$val" in + ${quote}*${quote}|${tick}*${tick}) + val=${val#?}; val=${val%?};; + esac + _RET="$val" +} + +_read_config() { + # reads config from stdin, + # if no parameters are set, modifies _rc scoped environment vars. + # if keyname is provided, then returns found value of that key. + local keyname="${1:-_unset}" + local line="" hash="#" ckey="" key="" val="" + while read line; do + line=${line%%${hash}*} + key="${line%%:*}" + + # no : in the line. + [ "$key" = "$line" ] && continue + trim "$key" + key=${_RET} + + [ "$keyname" != "_unset" ] && [ "$keyname" != "$key" ] && + continue + + val="${line#*:}" + trim "$val" + unquote "${_RET}" + val=${_RET} + + if [ "$keyname" = "$key" ]; then + _RET="$val" + return 0 + fi + + case "$key" in + datasource) _rc_dsname="$val";; + policy) _rc_policy="$val";; + esac + done + if [ "$keyname" = "_unset" ]; then + return 1 + fi + _RET="" + return 0 +} + +parse_warn() { + echo "WARN: invalid value '$2' for key '$1'. Using $1=$3." 1>&2 +} + +parse_def_policy() { + local _rc_mode="" _rc_report="" _rc_found="" _rc_maybe="" _rc_notfound="" + local ret="" + parse_policy "$@" + ret=$? + _def_mode=$_rc_mode + _def_report=$_rc_report + _def_found=$_rc_found + _def_maybe=$_rc_maybe + _def_notfound=$_rc_notfound + return $ret +} + +parse_policy() { + # parse_policy(policy, default) + # parse a policy string. sets + # _rc_mode (enable|disable,search) + # _rc_report true|false + # _rc_found first|all + # _rc_maybe all|none + # _rc_notfound enable|disable + local def="" + case "$DI_UNAME_MACHINE" in + # these have dmi data + i?86|x86_64) def=${DI_DEFAULT_POLICY};; + # aarch64 has dmi, but not currently used (LP: #1663304) + aarch64) def=${DI_DEFAULT_POLICY_NO_DMI};; + *) def=${DI_DEFAULT_POLICY_NO_DMI};; + esac + local policy="$1" + local _def_mode="" _def_report="" _def_found="" _def_maybe="" + local _def_notfound="" + if [ $# -eq 1 ] || [ "$2" != "-" ]; then + def=${2:-${def}} + parse_def_policy "$def" - + fi + + local mode="" report="" found="" maybe="" notfound="" + local oifs="$IFS" tok="" val="" + IFS=","; set -- $policy; IFS="$oifs" + for tok in "$@"; do + val=${tok#*=} + case "$tok" in + report) report=true;; + $DI_ENABLED|$DI_DISABLED|search) mode=$tok;; + found=all|found=first) found=$val;; + maybe=all|maybe=none) maybe=$val;; + notfound=$DI_ENABLED|notfound=$DI_DISABLED) notfound=$val;; + found=*) + parse_warn found "$val" "${_def_found}" + found=${_def_found};; + maybe=*) + parse_warn maybe "$val" "${_def_maybe}" + maybe=${_def_maybe};; + notfound=*) + parse_warn notfound "$val" "${_def_notfound}" + notfound=${_def_notfound};; + esac + done + report=${report:-${_def_report:-false}} + _rc_report=${report} + _rc_mode=${mode:-${_def_mode}} + _rc_found=${found:-${_def_found}} + _rc_maybe=${maybe:-${_def_maybe}} + _rc_notfound=${notfound:-${_def_notfound}} +} + +read_config() { + local config="${PATH_DI_CONFIG}" + local _rc_dsname="" _rc_policy="" ret="" + if [ -f "$config" ]; then + _read_config < "$config" + ret=$? + elif [ -e "$config" ]; then + error "$config exists but is not a file!" + ret=1 + fi + local tok="" key="" val="" + for tok in ${DI_KERNEL_CMDLINE}; do + key=${tok%%=*} + val=${tok#*=} + case "$key" in + ci.ds) _rc_dsname="$val";; + ci.datasource) _rc_dsname="$val";; + ci.di.policy) _rc_policy="$val";; + esac + done + + local _rc_mode _rc_report _rc_found _rc_maybe _rc_notfound + parse_policy "${_rc_policy}" + debug 1 "policy loaded: mode=${_rc_mode} report=${_rc_report}" \ + "found=${_rc_found} maybe=${_rc_maybe} notfound=${_rc_notfound}" + DI_MODE=${_rc_mode} + DI_REPORT=${_rc_report} + DI_ON_FOUND=${_rc_found} + DI_ON_MAYBE=${_rc_maybe} + DI_ON_NOTFOUND=${_rc_notfound} + + DI_DSNAME="${_rc_dsname}" + return $ret +} + + +manual_clean_and_existing() { + [ -f "${PATH_VAR_LIB_CLOUD}/instance/manual-clean" ] +} + +read_uptime() { + local up idle + _RET="${UNAVAILABLE}" + [ -f "$PATH_PROC_UPTIME" ] && + read up idle < "$PATH_PROC_UPTIME" && _RET="$up" + return +} + +_main() { + local dscheck="" ret_dis=1 ret_en=0 + + read_uptime + debug 1 "[up ${_RET}s]" "ds-identify $*" + collect_info + + if [ "$DI_LOG" = "stderr" ]; then + _print_info 1>&2 + else + _print_info >> "$DI_LOG" + fi + + case "$DI_MODE" in + $DI_DISABLED) + debug 1 "mode=$DI_DISABLED. returning $ret_dis" + return $ret_dis + ;; + $DI_ENABLED) + debug 1 "mode=$DI_ENABLED. returning $ret_en" + return $ret_en;; + search) :;; + esac + + if [ -n "${DI_DSNAME}" ]; then + debug 1 "datasource '$DI_DSNAME' specified." + found "$DI_DSNAME" + return + fi + + if manual_clean_and_existing; then + debug 1 "manual_cache_clean enabled. Not writing datasource_list." + write_result "# manual_cache_clean." + return + fi + + # if there is only a single entry in $DI_DSLIST + set -- $DI_DSLIST + if [ $# -eq 1 ] || [ $# -eq 2 -a "$2" = "None" ] ; then + debug 1 "single entry in datasource_list ($DI_DSLIST) use that." + found "$@" + return + fi + + local found="" ret="" ds="" maybe="" _RET_excfg="" + local exfound_cfg="" exmaybe_cfg="" + for ds in ${DI_DSLIST}; do + dscheck_fn="dscheck_${ds}" + debug 2 "Checking for datasource '$ds' via '$dscheck_fn'" + if ! type "$dscheck_fn" >/dev/null 2>&1; then + warn "No check method '$dscheck_fn' for datasource '$ds'" + continue + fi + _RET_excfg="" + $dscheck_fn + ret="$?" + case "$ret" in + $DS_FOUND) + debug 1 "check for '$ds' returned found"; + exfound_cfg="${exfound_cfg:+${exfound_cfg}${CR}}${_RET_excfg}" + found="${found} $ds";; + $DS_MAYBE) + debug 1 "check for '$ds' returned maybe"; + exmaybe_cfg="${exmaybe_cfg:+${exmaybe_cfg}${CR}}${_RET_excfg}" + maybe="${maybe} $ds";; + *) debug 2 "check for '$ds' returned not-found[$ret]";; + esac + done + + debug 2 "found=${found# } maybe=${maybe# }" + set -- $found + if [ $# -ne 0 ]; then + if [ $# -eq 1 ]; then + debug 1 "Found single datasource: $1" + else + # found=all + debug 1 "Found $# datasources found=${DI_ON_FOUND}: $*" + if [ "${DI_ON_FOUND}" = "first" ]; then + set -- "$1" + fi + fi + found "$@" -- "${exfound_cfg}" + return + fi + + set -- $maybe + if [ $# -ne 0 -a "${DI_ON_MAYBE}" != "none" ]; then + debug 1 "$# datasources returned maybe: $*" + found "$@" -- "${exmaybe_cfg}" + return + fi + + # record the empty result. + record_notfound + case "$DI_ON_NOTFOUND" in + $DI_DISABLED) + debug 1 "No result. notfound=$DI_DISABLED. returning $ret_dis." + return $ret_dis + ;; + $DI_ENABLED) + debug 1 "No result. notfound=$DI_ENABLED. returning $ret_en" + return $ret_en;; + esac + + error "Unexpected result" + return 3 +} + +main() { + local ret="" + [ -d "$PATH_RUN_CI" ] || mkdir -p "$PATH_RUN_CI" + if [ "${1:+$1}" != "--force" ] && [ -f "$PATH_RUN_CI_CFG" ] && + [ -f "$PATH_RUN_DI_RESULT" ]; then + if read ret < "$PATH_RUN_DI_RESULT"; then + if [ "$ret" = "0" ] || [ "$ret" = "1" ]; then + debug 2 "used cached result $ret. pass --force to re-run." + return $ret; + fi + debug 1 "previous run returned unexpected '$ret'. Re-running." + else + error "failed to read result from $PATH_RUN_DI_RESULT!" + fi + fi + _main "$@" + ret=$? + echo "$ret" > "$PATH_RUN_DI_RESULT" + read_uptime + debug 1 "[up ${_RET}s]" "returning $ret" + return $ret +} + +noop() { + : +} + +case "${DI_MAIN}" in + main|print_info|noop) "${DI_MAIN}" "$@";; + *) error "unexpected value for DI_MAIN"; exit 1;; +esac + +# vi: syntax=sh ts=4 expandtab diff --git a/tools/make-mime.py b/tools/make-mime.py index 12727126..f6a72044 100755 --- a/tools/make-mime.py +++ b/tools/make-mime.py @@ -22,7 +22,7 @@ def file_content_type(text): try: filename, content_type = text.split(":", 1) return (open(filename, 'r'), filename, content_type.strip()) - except: + except ValueError: raise argparse.ArgumentError("Invalid value for %r" % (text)) diff --git a/tools/make-tarball b/tools/make-tarball index c150dd2f..91c45624 100755 --- a/tools/make-tarball +++ b/tools/make-tarball @@ -35,7 +35,7 @@ while [ $# -ne 0 ]; do done rev=${1:-HEAD} -version=$(git describe ${long_opt} $rev) +version=$(git describe "--match=[0-9]*" ${long_opt} $rev) archive_base="cloud-init-$version" if [ -z "$output" ]; then diff --git a/tools/mock-meta.py b/tools/mock-meta.py index d74f9e31..95fc4659 100755 --- a/tools/mock-meta.py +++ b/tools/mock-meta.py @@ -18,10 +18,10 @@ Then: """ import functools -import httplib import json import logging import os +import socket import random import string import sys @@ -29,7 +29,13 @@ import yaml from optparse import OptionParser -from BaseHTTPServer import (HTTPServer, BaseHTTPRequestHandler) +try: + from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler + import httplib as hclient +except ImportError: + from http.server import HTTPServer, BaseHTTPRequestHandler + from http import client as hclient + log = logging.getLogger('meta-server') @@ -183,6 +189,10 @@ def get_ssh_keys(): return keys +class HTTPServerV6(HTTPServer): + address_family = socket.AF_INET6 + + class MetaDataHandler(object): def __init__(self, opts): @@ -249,8 +259,11 @@ class MetaDataHandler(object): try: key_id = int(mybe_key) key_name = key_ids[key_id] - except: - raise WebException(httplib.BAD_REQUEST, + except ValueError: + raise WebException(hclient.BAD_REQUEST, + "%s: not an integer" % mybe_key) + except KeyError: + raise WebException(hclient.BAD_REQUEST, "Unknown key id %r" % mybe_key) # Extract the possible sub-params result = traverse(nparams[1:], { @@ -342,13 +355,13 @@ class Ec2Handler(BaseHTTPRequestHandler): return self._get_versions date = segments[0].strip().lower() if date not in self._get_versions(): - raise WebException(httplib.BAD_REQUEST, + raise WebException(hclient.BAD_REQUEST, "Unknown version format %r" % date) if len(segments) < 2: - raise WebException(httplib.BAD_REQUEST, "No action provided") + raise WebException(hclient.BAD_REQUEST, "No action provided") look_name = segments[1].lower() if look_name not in func_mapping: - raise WebException(httplib.BAD_REQUEST, + raise WebException(hclient.BAD_REQUEST, "Unknown requested data %r" % look_name) base_func = func_mapping[look_name] who = self.address_string() @@ -371,16 +384,16 @@ class Ec2Handler(BaseHTTPRequestHandler): data = func() if not data: data = '' - self.send_response(httplib.OK) + self.send_response(hclient.OK) self.send_header("Content-Type", "binary/octet-stream") self.send_header("Content-Length", len(data)) log.info("Sending data (len=%s):\n%s", len(data), format_text(data)) self.end_headers() - self.wfile.write(data) + self.wfile.write(data.encode()) except RuntimeError as e: log.exception("Error somewhere in the server.") - self.send_error(httplib.INTERNAL_SERVER_ERROR, message=str(e)) + self.send_error(hclient.INTERNAL_SERVER_ERROR, message=str(e)) except WebException as e: code = e.code log.exception(str(e)) @@ -408,7 +421,7 @@ def extract_opts(): help=("port from which to serve traffic" " (default: %default)")) parser.add_option("-a", "--addr", dest="address", action="store", type=str, - default='0.0.0.0', metavar="ADDRESS", + default='::', metavar="ADDRESS", help=("address from which to serve traffic" " (default: %default)")) parser.add_option("-f", '--user-data-file', dest='user_data_file', @@ -444,7 +457,7 @@ def run_server(): setup_fetchers(opts) log.info("CLI opts: %s", opts) server_address = (opts['address'], opts['port']) - server = HTTPServer(server_address, Ec2Handler) + server = HTTPServerV6(server_address, Ec2Handler) sa = server.socket.getsockname() log.info("Serving ec2 metadata on %s using port %s ...", sa[0], sa[1]) server.serve_forever() diff --git a/tools/read-version b/tools/read-version index 3b30b497..ddb28383 100755 --- a/tools/read-version +++ b/tools/read-version @@ -56,7 +56,7 @@ if os.path.isdir(os.path.join(_tdir, ".git")) and which("git"): flags = [] if use_tags: flags = ['--tags'] - cmd = ['git', 'describe'] + flags + cmd = ['git', 'describe', '--match=[0-9]*'] + flags version = tiny_p(cmd).strip() diff --git a/tools/validate-yaml.py b/tools/validate-yaml.py index d8bbcfcb..a57ea847 100755 --- a/tools/validate-yaml.py +++ b/tools/validate-yaml.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python """Try to read a YAML file and report any errors. """ @@ -79,3 +79,11 @@ deps = jsonpatch==1.2 six==1.9.0 -r{toxinidir}/test-requirements.txt + +[testenv:tip-pycodestyle] +commands = {envpython} -m pycodestyle {posargs:cloudinit/ tests/ tools/} +deps = pycodestyle + +[testenv:tip-pyflakes] +commands = {envpython} -m pyflakes {posargs:cloudinit/ tests/ tools/} +deps = pyflakes |