summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/config/cc_apt_update_upgrade.py3
-rw-r--r--cloudinit/config/cc_emit_upstart.py48
-rw-r--r--cloudinit/config/cc_final_message.py17
-rw-r--r--cloudinit/config/cc_resizefs.py4
-rw-r--r--cloudinit/config/cc_rightscale_userdata.py2
-rw-r--r--cloudinit/config/cc_update_etc_hosts.py6
-rw-r--r--cloudinit/config/cc_write_files.py102
-rw-r--r--cloudinit/handlers/__init__.py8
-rw-r--r--cloudinit/helpers.py3
-rw-r--r--cloudinit/log.py37
-rw-r--r--cloudinit/sources/DataSourceEc2.py2
-rw-r--r--cloudinit/sources/DataSourceMAAS.py91
-rw-r--r--cloudinit/sources/DataSourceOVF.py2
-rw-r--r--cloudinit/stages.py13
-rw-r--r--cloudinit/templater.py11
-rw-r--r--cloudinit/url_helper.py2
-rw-r--r--cloudinit/user_data.py2
-rw-r--r--cloudinit/util.py103
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