diff options
Diffstat (limited to 'cloudinit')
-rw-r--r-- | cloudinit/config/cc_apt_update_upgrade.py | 3 | ||||
-rw-r--r-- | cloudinit/config/cc_emit_upstart.py | 48 | ||||
-rw-r--r-- | cloudinit/config/cc_final_message.py | 17 | ||||
-rw-r--r-- | cloudinit/config/cc_resizefs.py | 4 | ||||
-rw-r--r-- | cloudinit/config/cc_rightscale_userdata.py | 2 | ||||
-rw-r--r-- | cloudinit/config/cc_update_etc_hosts.py | 6 | ||||
-rw-r--r-- | cloudinit/config/cc_write_files.py | 102 | ||||
-rw-r--r-- | cloudinit/handlers/__init__.py | 8 | ||||
-rw-r--r-- | cloudinit/helpers.py | 3 | ||||
-rw-r--r-- | cloudinit/log.py | 37 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceEc2.py | 2 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceMAAS.py | 91 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceOVF.py | 2 | ||||
-rw-r--r-- | cloudinit/stages.py | 13 | ||||
-rw-r--r-- | cloudinit/templater.py | 11 | ||||
-rw-r--r-- | cloudinit/url_helper.py | 2 | ||||
-rw-r--r-- | cloudinit/user_data.py | 2 | ||||
-rw-r--r-- | cloudinit/util.py | 103 |
18 files changed, 389 insertions, 67 deletions
diff --git a/cloudinit/config/cc_apt_update_upgrade.py b/cloudinit/config/cc_apt_update_upgrade.py index 5c5e510c..1bffa47d 100644 --- a/cloudinit/config/cc_apt_update_upgrade.py +++ b/cloudinit/config/cc_apt_update_upgrade.py @@ -255,7 +255,8 @@ def find_apt_mirror(cloud, cfg): if mydom: doms.append(".%s" % mydom) - if not mirror: + if (not mirror and + util.get_cfg_option_bool(cfg, "apt_mirror_search_dns", False)): doms.extend((".localdomain", "",)) mirror_list = [] diff --git a/cloudinit/config/cc_emit_upstart.py b/cloudinit/config/cc_emit_upstart.py new file mode 100644 index 00000000..68b86ff6 --- /dev/null +++ b/cloudinit/config/cc_emit_upstart.py @@ -0,0 +1,48 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2009-2011 Canonical Ltd. +# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# +# Author: Scott Moser <scott.moser@canonical.com> +# Author: Juerg Haefliger <juerg.haefliger@hp.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from cloudinit import util +from cloudinit.settings import PER_ALWAYS + +frequency = PER_ALWAYS + +distros = ['ubuntu', 'debian'] + + +def handle(name, _cfg, cloud, log, args): + event_names = args + if not event_names: + # Default to the 'cloud-config' + # event for backwards compat. + event_names = ['cloud-config'] + if not os.path.isfile("/sbin/initctl"): + log.debug(("Skipping module named %s," + " no /sbin/initctl located"), name) + return + cfgpath = cloud.paths.get_ipath_cur("cloud_config") + for n in event_names: + cmd = ['initctl', 'emit', str(n), 'CLOUD_CFG=%s' % cfgpath] + try: + util.subp(cmd) + except Exception as e: + # TODO, use log exception from utils?? + log.warn("Emission of upstart event %s failed due to: %s", n, e) diff --git a/cloudinit/config/cc_final_message.py b/cloudinit/config/cc_final_message.py index b1caca47..aff03c4e 100644 --- a/cloudinit/config/cc_final_message.py +++ b/cloudinit/config/cc_final_message.py @@ -26,23 +26,20 @@ from cloudinit.settings import PER_ALWAYS frequency = PER_ALWAYS -FINAL_MESSAGE_DEF = ("Cloud-init v. {{version}} finished at {{timestamp}}." - " Up {{uptime}} seconds.") +# Cheetah formated default message +FINAL_MESSAGE_DEF = ("Cloud-init v. ${version} finished at ${timestamp}." + " Up ${uptime} seconds.") def handle(_name, cfg, cloud, log, args): - msg_in = None + msg_in = '' if len(args) != 0: - msg_in = args[0] + msg_in = str(args[0]) else: - msg_in = util.get_cfg_option_str(cfg, "final_message") - - if not msg_in: - template_fn = cloud.get_template_filename('final_message') - if template_fn: - msg_in = util.load_file(template_fn) + msg_in = util.get_cfg_option_str(cfg, "final_message", "") + msg_in = msg_in.strip() if not msg_in: msg_in = FINAL_MESSAGE_DEF diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py index 69cd8872..256a194f 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -134,7 +134,7 @@ def do_resize(resize_cmd, log): except util.ProcessExecutionError: util.logexc(log, "Failed to resize filesystem (cmd=%s)", resize_cmd) raise - tot_time = int(time.time() - start) - log.debug("Resizing took %s seconds", tot_time) + tot_time = time.time() - start + log.debug("Resizing took %.3f seconds", tot_time) # TODO: Should we add a fsck check after this to make # sure we didn't corrupt anything? diff --git a/cloudinit/config/cc_rightscale_userdata.py b/cloudinit/config/cc_rightscale_userdata.py index 7a134569..45d41b3f 100644 --- a/cloudinit/config/cc_rightscale_userdata.py +++ b/cloudinit/config/cc_rightscale_userdata.py @@ -53,7 +53,7 @@ def handle(name, _cfg, cloud, log, _args): try: ud = cloud.get_userdata_raw() except: - log.warn("Failed to get raw userdata in module %s", name) + log.debug("Failed to get raw userdata in module %s", name) return try: diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py index c148b12e..38108da7 100644 --- a/cloudinit/config/cc_update_etc_hosts.py +++ b/cloudinit/config/cc_update_etc_hosts.py @@ -36,11 +36,11 @@ def handle(name, cfg, cloud, log, _args): return # Render from a template file - distro_n = cloud.distro.name - tpl_fn_name = cloud.get_template_filename("hosts.%s" % (distro_n)) + tpl_fn_name = cloud.get_template_filename("hosts.%s" % + (cloud.distro.name)) if not tpl_fn_name: raise RuntimeError(("No hosts template could be" - " found for distro %s") % (distro_n)) + " found for distro %s") % (cloud.distro.name)) out_fn = cloud.paths.join(False, '/etc/hosts') templater.render_to_file(tpl_fn_name, out_fn, diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py new file mode 100644 index 00000000..1bfa4c25 --- /dev/null +++ b/cloudinit/config/cc_write_files.py @@ -0,0 +1,102 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow <harlowja@yahoo-inc.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import base64 +import os + +from cloudinit import util +from cloudinit.settings import PER_INSTANCE + +frequency = PER_INSTANCE + +DEFAULT_OWNER = "root:root" +DEFAULT_PERMS = 0644 +UNKNOWN_ENC = 'text/plain' + + +def handle(name, cfg, _cloud, log, _args): + files = cfg.get('write_files') + if not files: + log.debug(("Skipping module named %s," + " no/empty 'write_files' key in configuration"), name) + return + write_files(name, files, log) + + +def canonicalize_extraction(encoding_type, log): + if not encoding_type: + encoding_type = '' + encoding_type = encoding_type.lower().strip() + if encoding_type in ['gz', 'gzip']: + return ['application/x-gzip'] + if encoding_type in ['gz+base64', 'gzip+base64', 'gz+b64', 'gzip+b64']: + return ['application/base64', 'application/x-gzip'] + # Yaml already encodes binary data as base64 if it is given to the + # yaml file as binary, so those will be automatically decoded for you. + # But the above b64 is just for people that are more 'comfortable' + # specifing it manually (which might be a possiblity) + if encoding_type in ['b64', 'base64']: + return ['application/base64'] + if encoding_type: + log.warn("Unknown encoding type %s, assuming %s", + encoding_type, UNKNOWN_ENC) + return [UNKNOWN_ENC] + + +def write_files(name, files, log): + if not files: + return + + for (i, f_info) in enumerate(files): + path = f_info.get('path') + if not path: + log.warn("No path provided to write for entry %s in module %s", + i + 1, name) + continue + path = os.path.abspath(path) + extractions = canonicalize_extraction(f_info.get('encoding'), log) + contents = extract_contents(f_info.get('content', ''), extractions) + (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER)) + perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS, log) + util.write_file(path, contents, mode=perms) + util.chownbyname(path, u, g) + + +def decode_perms(perm, default, log): + try: + if isinstance(perm, (int, long, float)): + # Just 'downcast' it (if a float) + return int(perm) + else: + # Force to string and try octal conversion + return int(str(perm), 8) + except (TypeError, ValueError): + log.warn("Undecodable permissions %s, assuming %s", perm, default) + return default + + +def extract_contents(contents, extraction_types): + result = str(contents) + for t in extraction_types: + if t == 'application/x-gzip': + result = util.decomp_gzip(result, quiet=False) + elif t == 'application/base64': + result = base64.b64decode(result) + elif t == UNKNOWN_ENC: + pass + return result diff --git a/cloudinit/handlers/__init__.py b/cloudinit/handlers/__init__.py index dce2abef..6d1502f4 100644 --- a/cloudinit/handlers/__init__.py +++ b/cloudinit/handlers/__init__.py @@ -165,7 +165,10 @@ def walker_callback(pdata, ctype, filename, payload): walker_handle_handler(pdata, ctype, filename, payload) return handlers = pdata['handlers'] - if ctype not in pdata['handlers'] and payload: + if ctype in pdata['handlers']: + run_part(handlers[ctype], pdata['data'], ctype, filename, + payload, pdata['frequency']) + elif payload: # Extract the first line or 24 bytes for displaying in the log start = _extract_first_or_bytes(payload, 24) details = "'%s...'" % (start.encode("string-escape")) @@ -176,8 +179,7 @@ def walker_callback(pdata, ctype, filename, payload): LOG.warning("Unhandled unknown content-type (%s) userdata: %s", ctype, details) else: - run_part(handlers[ctype], pdata['data'], ctype, filename, - payload, pdata['frequency']) + LOG.debug("empty payload of type %s" % ctype) # Callback is a function that will be called with diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py index 15036a50..a4b20208 100644 --- a/cloudinit/helpers.py +++ b/cloudinit/helpers.py @@ -67,6 +67,9 @@ class FileLock(object): def __init__(self, fn): self.fn = fn + def __str__(self): + return "<%s using file %r>" % (util.obj_name(self), self.fn) + class FileSemaphores(object): def __init__(self, sem_path): diff --git a/cloudinit/log.py b/cloudinit/log.py index fc1428a2..819c85b6 100644 --- a/cloudinit/log.py +++ b/cloudinit/log.py @@ -24,6 +24,7 @@ import logging import logging.handlers import logging.config +import collections import os import sys @@ -63,9 +64,11 @@ def setupLogging(cfg=None): # If there is a 'logcfg' entry in the config, # respect it, it is the old keyname log_cfgs.append(str(log_cfg)) - elif "log_cfgs" in cfg and isinstance(cfg['log_cfgs'], (set, list)): + elif "log_cfgs" in cfg: for a_cfg in cfg['log_cfgs']: - if isinstance(a_cfg, (list, set, dict)): + if isinstance(a_cfg, (basestring, str)): + log_cfgs.append(a_cfg) + elif isinstance(a_cfg, (collections.Iterable)): cfg_str = [str(c) for c in a_cfg] log_cfgs.append('\n'.join(cfg_str)) else: @@ -73,30 +76,36 @@ def setupLogging(cfg=None): # See if any of them actually load... am_tried = 0 - am_worked = 0 - for i, log_cfg in enumerate(log_cfgs): + for log_cfg in log_cfgs: try: am_tried += 1 # Assume its just a string if not a filename if log_cfg.startswith("/") and os.path.isfile(log_cfg): + # Leave it as a file and do not make it look like + # something that is a file (but is really a buffer that + # is acting as a file) pass else: log_cfg = StringIO(log_cfg) # Attempt to load its config logging.config.fileConfig(log_cfg) - am_worked += 1 - except Exception as e: - sys.stderr.write(("WARN: Setup of logging config %s" - " failed due to: %s\n") % (i + 1, e)) + # The first one to work wins! + return + except Exception: + # We do not write any logs of this here, because the default + # configuration includes an attempt at using /dev/log, followed + # up by writing to a file. /dev/log will not exist in very early + # boot, so an exception on that is expected. + pass # If it didn't work, at least setup a basic logger (if desired) basic_enabled = cfg.get('log_basic', True) - if not am_worked: - sys.stderr.write(("WARN: no logging configured!" - " (tried %s configs)\n") % (am_tried)) - if basic_enabled: - sys.stderr.write("Setting up basic logging...\n") - setupBasicLogging() + + sys.stderr.write(("WARN: no logging configured!" + " (tried %s configs)\n") % (am_tried)) + if basic_enabled: + sys.stderr.write("Setting up basic logging...\n") + setupBasicLogging() def getLogger(name='cloudinit'): diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index cde73de3..d9eb8f17 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -230,7 +230,7 @@ class DataSourceEc2(sources.DataSource): remapped = self._remap_device(os.path.basename(found)) if remapped: - LOG.debug("Remapped device name %s => %s", (found, remapped)) + LOG.debug("Remapped device name %s => %s", found, remapped) return remapped # On t1.micro, ephemeral0 will appear in block-device-mapping from diff --git a/cloudinit/sources/DataSourceMAAS.py b/cloudinit/sources/DataSourceMAAS.py index f16d5c21..c568d365 100644 --- a/cloudinit/sources/DataSourceMAAS.py +++ b/cloudinit/sources/DataSourceMAAS.py @@ -262,3 +262,94 @@ datasources = [ # Return a list of data sources that match this set of dependencies def get_datasource_list(depends): return sources.list_from_depends(depends, datasources) + + +if __name__ == "__main__": + def main(): + """ + Call with single argument of directory or http or https url. + If url is given additional arguments are allowed, which will be + interpreted as consumer_key, token_key, token_secret, consumer_secret + """ + import argparse + import pprint + + parser = argparse.ArgumentParser(description='Interact with MAAS DS') + parser.add_argument("--config", metavar="file", + help="specify DS config file", default=None) + parser.add_argument("--ckey", metavar="key", + help="the consumer key to auth with", default=None) + parser.add_argument("--tkey", metavar="key", + help="the token key to auth with", default=None) + parser.add_argument("--csec", metavar="secret", + help="the consumer secret (likely '')", default="") + parser.add_argument("--tsec", metavar="secret", + help="the token secret to auth with", default=None) + parser.add_argument("--apiver", metavar="version", + help="the apiver to use ("" can be used)", default=MD_VERSION) + + subcmds = parser.add_subparsers(title="subcommands", dest="subcmd") + subcmds.add_parser('crawl', help="crawl the datasource") + subcmds.add_parser('get', help="do a single GET of provided url") + subcmds.add_parser('check-seed', help="read andn verify seed at url") + + parser.add_argument("url", help="the data source to query") + + args = parser.parse_args() + + creds = {'consumer_key': args.ckey, 'token_key': args.tkey, + 'token_secret': args.tsec, 'consumer_secret': args.csec} + + if args.config: + import yaml + with open(args.config) as fp: + cfg = yaml.safe_load(fp) + if 'datasource' in cfg: + cfg = cfg['datasource']['MAAS'] + for key in creds.keys(): + if key in cfg and creds[key] is None: + creds[key] = cfg[key] + + def geturl(url, headers_cb): + req = urllib2.Request(url, data=None, headers=headers_cb(url)) + return(urllib2.urlopen(req).read()) + + def printurl(url, headers_cb): + print "== %s ==\n%s\n" % (url, geturl(url, headers_cb)) + + def crawl(url, headers_cb=None): + if url.endswith("/"): + for line in geturl(url, headers_cb).splitlines(): + if line.endswith("/"): + crawl("%s%s" % (url, line), headers_cb) + else: + printurl("%s%s" % (url, line), headers_cb) + else: + printurl(url, headers_cb) + + def my_headers(url): + headers = {} + if creds.get('consumer_key', None) is not None: + headers = oauth_headers(url, **creds) + return headers + + if args.subcmd == "check-seed": + if args.url.startswith("http"): + (userdata, metadata) = read_maas_seed_url(args.url, + header_cb=my_headers, version=args.apiver) + else: + (userdata, metadata) = read_maas_seed_url(args.url) + print "=== userdata ===" + print userdata + print "=== metadata ===" + pprint.pprint(metadata) + + elif args.subcmd == "get": + printurl(args.url, my_headers) + + elif args.subcmd == "crawl": + if not args.url.endswith("/"): + args.url = "%s/" % args.url + crawl(args.url, my_headers) + + main() diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py index 7728b36f..771e64eb 100644 --- a/cloudinit/sources/DataSourceOVF.py +++ b/cloudinit/sources/DataSourceOVF.py @@ -213,7 +213,7 @@ def transport_iso9660(require_iso=True): (fname, contents) = util.mount_cb(fullp, get_ovf_env, mtype="iso9660") except util.MountFailedError: - util.logexc(LOG, "Failed mounting %s", fullp) + LOG.debug("%s not mountable as iso9660" % fullp) continue if contents is not False: diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 8fd6aa5d..2f6a566c 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -133,12 +133,13 @@ class Init(object): if log_file: util.ensure_file(log_file) if perms: - (u, g) = perms.split(':', 1) - if u == "-1" or u == "None": - u = None - if g == "-1" or g == "None": - g = None - util.chownbyname(log_file, u, g) + u, g = util.extract_usergroup(perms) + try: + util.chownbyname(log_file, u, g) + except OSError: + util.logexc(LOG, ("Unable to change the ownership" + " of %s to user %s, group %s"), + log_file, u, g) def read_cfg(self, extra_fns=None): # None check so that we don't keep on re-loading if empty diff --git a/cloudinit/templater.py b/cloudinit/templater.py index c4259fa0..77af1270 100644 --- a/cloudinit/templater.py +++ b/cloudinit/templater.py @@ -20,13 +20,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from tempita import Template +from Cheetah.Template import Template from cloudinit import util def render_from_file(fn, params): - return render_string(util.load_file(fn), params, name=fn) + return render_string(util.load_file(fn), params) def render_to_file(fn, outfn, params, mode=0644): @@ -34,8 +34,7 @@ def render_to_file(fn, outfn, params, mode=0644): util.write_file(outfn, contents, mode=mode) -def render_string(content, params, name=None): - tpl = Template(content, name=name) +def render_string(content, params): if not params: - params = dict() - return tpl.substitute(params) + params = {} + return Template(content, searchList=[params]).respond() diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py index dbf72392..732d6aec 100644 --- a/cloudinit/url_helper.py +++ b/cloudinit/url_helper.py @@ -127,7 +127,7 @@ def readurl(url, data=None, timeout=None, time.sleep(sec_between) # Didn't work out - LOG.warn("Failed reading from %s after %s attempts", url, attempts) + LOG.debug("Failed reading from %s after %s attempts", url, attempts) # It must of errored at least once for code # to get here so re-raise the last error diff --git a/cloudinit/user_data.py b/cloudinit/user_data.py index 0842594d..f5d01818 100644 --- a/cloudinit/user_data.py +++ b/cloudinit/user_data.py @@ -227,7 +227,7 @@ def convert_string(raw_data, headers=None): raw_data = '' if not headers: headers = {} - data = util.decomp_str(raw_data) + data = util.decomp_gzip(raw_data) if "mime-version:" in data[0:4096].lower(): msg = email.message_from_string(data) for (key, val) in headers.iteritems(): diff --git a/cloudinit/util.py b/cloudinit/util.py index 44ce9770..a8c0cceb 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -55,6 +55,7 @@ from cloudinit import url_helper as uhelp from cloudinit.settings import (CFG_BUILTIN) +_DNS_REDIRECT_IP = None LOG = logging.getLogger(__name__) # Helps cleanup filenames to ensure they aren't FS incompatible @@ -159,6 +160,10 @@ class MountFailedError(Exception): pass +class DecompressionError(Exception): + pass + + def ExtendedTemporaryFile(**kwargs): fh = tempfile.NamedTemporaryFile(**kwargs) # Replace its unlink with a quiet version @@ -256,13 +261,32 @@ def clean_filename(fn): return fn -def decomp_str(data): +def decomp_gzip(data, quiet=True): try: buf = StringIO(str(data)) with contextlib.closing(gzip.GzipFile(None, "rb", 1, buf)) as gh: return gh.read() - except: - return data + except Exception as e: + if quiet: + return data + else: + raise DecompressionError(str(e)) + + +def extract_usergroup(ug_pair): + if not ug_pair: + return (None, None) + ug_parted = ug_pair.split(':', 1) + u = ug_parted[0].strip() + if len(ug_parted) == 2: + g = ug_parted[1].strip() + else: + g = None + if not u or u == "-1" or u.lower() == "none": + u = None + if not g or g == "-1" or g.lower() == "none": + g = None + return (u, g) def find_modules(root_dir): @@ -288,8 +312,10 @@ def multi_log(text, console=True, stderr=True, wfh.write(text) wfh.flush() if log: - log.log(log_level, text) - + if text[-1] == "\n": + log.log(log_level, text[:-1]) + else: + log.log(log_level, text) def is_ipv4(instr): """ determine if input string is a ipv4 address. return boolean""" @@ -381,7 +407,16 @@ def fixup_output(cfg, mode): # # with a '|', arguments are passed to shell, so one level of # shell escape is required. +# +# if _CLOUD_INIT_SAVE_STDOUT is set in environment to a non empty and true +# value then output input will not be closed (useful for debugging). +# def redirect_output(outfmt, errfmt, o_out=None, o_err=None): + + if is_true(os.environ.get("_CLOUD_INIT_SAVE_STDOUT")): + LOG.debug("Not redirecting output due to _CLOUD_INIT_SAVE_STDOUT") + return + if not o_out: o_out = sys.stdout if not o_err: @@ -535,7 +570,7 @@ def runparts(dirp, skip_no_exist=True): if os.path.isfile(exe_path) and os.access(exe_path, os.X_OK): attempted.append(exe_path) try: - subp([exe_path]) + subp([exe_path], capture=False) except ProcessExecutionError as e: logexc(LOG, "Failed running %s [%s]", exe_path, e.exit_code) failed.append(e) @@ -584,7 +619,10 @@ def load_yaml(blob, default=None, allowed=(dict,)): (allowed, obj_name(converted))) loaded = converted except (yaml.YAMLError, TypeError, ValueError): - logexc(LOG, "Failed loading yaml blob") + if len(blob) == 0: + LOG.debug("load_yaml given empty string, returning default") + else: + logexc(LOG, "Failed loading yaml blob") return loaded @@ -788,9 +826,43 @@ def get_cmdline_url(names=('cloud-config-url', 'url'), def is_resolvable(name): - """ determine if a url is resolvable, return a boolean """ + """ determine if a url is resolvable, return a boolean + This also attempts to be resilent against dns redirection. + + Note, that normal nsswitch resolution is used here. So in order + to avoid any utilization of 'search' entries in /etc/resolv.conf + we have to append '.'. + + The top level 'invalid' domain is invalid per RFC. And example.com + should also not exist. The random entry will be resolved inside + the search list. + """ + global _DNS_REDIRECT_IP # pylint: disable=W0603 + if _DNS_REDIRECT_IP is None: + badips = set() + badnames = ("does-not-exist.example.com.", "example.invalid.", + rand_str()) + badresults = {} + for iname in badnames: + try: + result = socket.getaddrinfo(iname, None, 0, 0, + socket.SOCK_STREAM, socket.AI_CANONNAME) + badresults[iname] = [] + for (_fam, _stype, _proto, cname, sockaddr) in result: + badresults[iname].append("%s: %s" % (cname, sockaddr[0])) + badips.add(sockaddr[0]) + except socket.gaierror: + pass + _DNS_REDIRECT_IP = badips + if badresults: + LOG.debug("detected dns redirection: %s" % badresults) + try: - socket.getaddrinfo(name, None) + result = socket.getaddrinfo(name, None) + # check first result's sockaddr field + addr = result[0][4][0] + if addr in _DNS_REDIRECT_IP: + return False return True except socket.gaierror: return False @@ -825,10 +897,10 @@ def close_stdin(): reopen stdin as /dev/null so even subprocesses or other os level things get /dev/null as input. - if _CLOUD_INIT_SAVE_STDIN is set in environment to a non empty or '0' value - then input will not be closed (only useful potentially for debugging). + if _CLOUD_INIT_SAVE_STDIN is set in environment to a non empty and true + value then input will not be closed (useful for debugging). """ - if os.environ.get("_CLOUD_INIT_SAVE_STDIN") in ("", "0", 'False'): + if is_true(os.environ.get("_CLOUD_INIT_SAVE_STDIN")): return with open(os.devnull) as fp: os.dup2(fp.fileno(), sys.stdin.fileno()) @@ -937,12 +1009,9 @@ def chownbyname(fname, user=None, group=None): uid = pwd.getpwnam(user).pw_uid if group: gid = grp.getgrnam(group).gr_gid - except KeyError: - logexc(LOG, ("Failed changing the ownership of %s using username %s " - "and groupname %s (do they exist?)"), fname, user, group) - return False + except KeyError as e: + raise OSError("Unknown user or group: %s" % (e)) chownbyid(fname, uid, gid) - return True # Always returns well formated values |