diff options
author | Scott Moser <smoser@ubuntu.com> | 2012-07-06 17:19:37 -0400 |
---|---|---|
committer | Scott Moser <smoser@ubuntu.com> | 2012-07-06 17:19:37 -0400 |
commit | b2a21ed1dc682a262d55a4202c6b9606496d211f (patch) | |
tree | 37f1c4acd3ea891c1e7a60bcd9dd8aa8c7ca1e0c /cloudinit/util.py | |
parent | 646384ccd6f1707b2712d9bcd683ae877f1903bd (diff) | |
parent | e7095a1b19e849c530650d2d71edf8b28d30f1d1 (diff) | |
download | vyos-cloud-init-b2a21ed1dc682a262d55a4202c6b9606496d211f.tar.gz vyos-cloud-init-b2a21ed1dc682a262d55a4202c6b9606496d211f.zip |
Merge rework branch in [Joshua Harlow]
- unified binary that activates the various stages
- Now using argparse + subcommands to specify the various CLI options
- a stage module that clearly separates the stages of the different
components (also described how they are used and in what order in the
new unified binary)
- user_data is now a module that just does user data processing while the
actual activation and 'handling' of the processed user data is done via
a separate set of files (and modules) with the main 'init' stage being the
controller of this
- creation of boot_hook, cloud_config, shell_script, upstart_job version 2
modules (with classes that perform there functionality) instead of those
having functionality that is attached to the cloudinit object (which
reduces reuse and limits future functionality, and makes testing harder)
- removal of global config that defined paths, shared config, now this is
via objects making unit testing testing and global side-effects a non issue
- creation of a 'helpers.py'
- this contains an abstraction for the 'lock' like objects that the various
module/handler running stages use to avoid re-running a given
module/handler for a given frequency. this makes it separated from
the actual usage of that object (thus helpful for testing and clear lines
usage and how the actual job is accomplished)
- a common 'runner' class is the main entrypoint using these locks to
run function objects passed in (along with there arguments) and there
frequency
- add in a 'paths' object that provides access to the previously global
and/or config based paths (thus providing a single entrypoint object/type
that provides path information)
- this also adds in the ability to change the path when constructing
that path 'object' and adding in additional config that can be used to
alter the root paths of 'joins' (useful for testing or possibly useful
in chroots?)
- config options now avaiable that can alter the 'write_root' and the
'read_root' when backing code uses the paths join() function
- add a config parser subclass that will automatically add unknown sections
and return default values (instead of throwing exceptions for these cases)
- a new config merging class that will be the central object that knows
how to do the common configuration merging from the various configuration
sources. The order is the following:
- cli config files override environment config files
which override instance configs which override datasource
configs which override base configuration which overrides
default configuration.
- remove the passing around of the 'cloudinit' object as a 'cloud' variable
and instead pass around an 'interface' object that can be given to modules
and handlers as there cloud access layer while the backing of that
object can be varied (good for abstraction and testing)
- use a single set of functions to do importing of modules
- add a function in which will search for a given set of module names with
a given set of attributes and return those which are found
- refactor logging so that instead of using a single top level 'log' that
instead each component/module can use its own logger (if desired), this
should be backwards compatible with handlers and config modules that used
the passed in logger (its still passed in)
- ensure that all places where exception are caught and where applicable
that the util logexc() is called, so that no exceptions that may occur
are dropped without first being logged (where it makes sense for this
to happen)
- add a 'requires' file that lists cloud-init dependencies
- applying it in package creation (bdeb and brpm) as well as using it
in the modified setup.py to ensure dependencies are installed when
using that method of packaging
- add a 'version.py' that lists the active version (in code) so that code
inside cloud-init can report the version in messaging and other config files
- cleanup of subprocess usage so that all subprocess calls go through the
subp() utility method, which now has an exception type that will provide
detailed information on python 2.6 and 2.7
- forced all code loading, moving, chmod, writing files and other system
level actions to go through standard set of util functions, this greatly
helps in debugging and determining exactly which system actions cloud-init is
performing
- switching out the templating engine cheetah for tempita since tempita has
no external dependencies (minus python) while cheetah has many dependencies
which makes it more difficult to adopt cloud-init in distros that may not
have those dependencies
- adjust url fetching and url trying to go through a single function that
reads urls in the new 'url helper' file, this helps in tracing, debugging
and knowing which urls are being called and/or posted to from with-in
cloud-init code
- add in the sending of a 'User-Agent' header for all urls fetched that
do not provide there own header mapping, derive this user-agent from
the following template, 'Cloud-Init/{version}' where the version is the
cloud-init version number
- using prettytable for netinfo 'debug' printing since it provides a standard
and defined output that should be easier to parse than a custom format
- add a set of distro specific classes, that handle distro specific actions
that modules and or handler code can use as needed, this is organized into
a base abstract class with child classes that implement the shared
functionality. config determines exactly which subclass to load, so it can
be easily extended as needed.
- current functionality
- network interface config file writing
- hostname setting/updating
- locale/timezone/ setting
- updating of /etc/hosts (with templates or generically)
- package commands (ie installing, removing)/mirror finding
- interface up/down activating
- implemented a debian + ubuntu subclass
- implemented a redhat + fedora subclass
- adjust the root 'cloud.cfg' file to now have distrobution/path specific
configuration values in it. these special configs are merged as the normal
config is, but the system level config is not passed into modules/handlers
- modules/handlers must go through the path and distro object instead
- have the cloudstack datasource test the url before calling into boto to
avoid the long wait for boto to finish retrying and finally fail when
the gateway meta-data address is unavailable
- add a simple mock ec2 meta-data python based http server that can serve a
very simple set of ec2 meta-data back to callers
- useful for testing or for understanding what the ec2 meta-data
service can provide in terms of data or functionality
- for ssh key and authorized key file parsing add in classes and util functions
that maintain the state of individual lines, allowing for a clearer
separation of parsing and modification (useful for testing and tracing)
- add a set of 'base' init.d scripts that can be used on systems that do
not have full upstart or systemd support (or support that does not match
the standard fedora/ubuntu implementation)
- currently these are being tested on RHEL 6.2
- separate the datasources into there own subdirectory (instead of being
a top-level item), this matches how config 'modules' and user-data 'handlers'
are also in there own subdirectory (thus helping new developers and others
understand the code layout in a quicker manner)
- add the building of rpms based off a new cli tool and template 'spec' file
that will templatize and perform the necessary commands to create a source
and binary package to be used with a cloud-init install on a 'rpm' supporting
system
- uses the new standard set of requires and converts those pypi requirements
into a local set of package requirments (that are known to exist on RHEL
systems but should also exist on fedora systems)
- adjust the bdeb builder to be a python script (instead of a shell script) and
make its 'control' file a template that takes in the standard set of pypi
dependencies and uses a local mapping (known to work on ubuntu) to create the
packages set of dependencies (that should also work on ubuntu-like systems)
- pythonify a large set of various pieces of code
- remove wrapping return statements with () when it has no effect
- upper case all constants used
- correctly 'case' class and method names (where applicable)
- use os.path.join (and similar commands) instead of custom path creation
- use 'is None' instead of the frowned upon '== None' which picks up a large
set of 'true' cases than is typically desired (ie for objects that have
there own equality)
- use context managers on locks, tempdir, chdir, file, selinux, umask,
unmounting commands so that these actions do not have to be closed and/or
cleaned up manually in finally blocks, which is typically not done and will
eventually be a bug in the future
- use the 'abc' module for abstract classes base where possible
- applied in the datasource root class, the distro root class, and the
user-data v2 root class
- when loading yaml, check that the 'root' type matches a predefined set of
valid types (typically just 'dict') and throw a type error if a mismatch
occurs, this seems to be a good idea to do when loading user config files
- when forking a long running task (ie resizing a filesytem) use a new util
function that will fork and then call a callback, instead of having to
implement all that code in a non-shared location (thus allowing it to be
used by others in the future)
- when writing out filenames, go through a util function that will attempt to
ensure that the given filename is 'filesystem' safe by replacing '/' with
'_' and removing characters which do not match a given whitelist of allowed
filename characters
- for the varying usages of the 'blkid' command make a function in the util
module that can be used as the single point of entry for interaction with
that command (and its results) instead of having X separate implementations
- place the rfc 8222 time formatting and uptime repeated pieces of code in the
util module as a set of function with the name 'time_rfc2822'/'uptime'
- separate the pylint+pep8 calling from one tool into two indivudal tools so
that they can be called independently, add make file sections that can be
used to call these independently
- remove the support for the old style config that was previously located in
'/etc/ec2-init/ec2-config.cfg', no longer supported!
- instead of using a altered config parser that added its own 'dummy' section
on in the 'mcollective' module, use configobj which handles the parsing of
config without sections better (and it also maintains comments instead of
removing them)
- use the new defaulting config parser (that will not raise errors on sections
that do not exist or return errors when values are fetched that do not exist)
in the 'puppet' module
- for config 'modules' add in the ability for the module to provide a list of
distro names which it is known to work with, if when ran and the distro being
used name does not match one of those in this list, a warning will be written
out saying that this module may not work correctly on this distrobution
- for all dynamically imported modules ensure that they are fixed up before
they are used by ensuring that they have certain attributes, if they do not
have those attributes they will be set to a sensible set of defaults instead
- adjust all 'config' modules and handlers to use the adjusted util functions
and the new distro objects where applicable so that those pieces of code can
benefit from the unified and enhanced functionality being provided in that
util module
- fix a potential bug whereby when a #includeonce was encountered it would
enable checking of urls against a cache, if later a #include was encountered
it would continue checking against that cache, instead of refetching (which
would likely be the expected case)
- add a openstack/nova based pep8 extension utility ('hacking.py') that allows
for custom checks (along with the standard pep8 checks) to occur when running
'make pep8' and its derivatives
Diffstat (limited to 'cloudinit/util.py')
-rw-r--r-- | cloudinit/util.py | 1636 |
1 files changed, 1090 insertions, 546 deletions
diff --git a/cloudinit/util.py b/cloudinit/util.py index 84aae3ea..d7dd20b5 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -1,10 +1,12 @@ # vi: ts=4 expandtab # -# Copyright (C) 2009-2010 Canonical Ltd. +# Copyright (C) 2012 Canonical Ltd. # Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# Copyright (C) 2012 Yahoo! Inc. # # Author: Scott Moser <scott.moser@canonical.com> -# Author: Juerg Hafliger <juerg.haefliger@hp.com> +# Author: Juerg Haefliger <juerg.haefliger@hp.com> +# 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 @@ -18,87 +20,314 @@ # 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 yaml +from StringIO import StringIO + +import copy as obj_copy +import contextlib +import errno +import glob +import grp +import gzip +import hashlib import os -import os.path +import platform +import pwd +import random import shutil -import errno -import subprocess -from Cheetah.Template import Template -import urllib2 -import urllib -import logging -import re import socket +import stat +import string # pylint: disable=W0402 +import subprocess import sys -import time import tempfile -import traceback +import time +import types import urlparse -try: - import selinux - HAVE_LIBSELINUX = True -except ImportError: - HAVE_LIBSELINUX = False +import yaml + +from cloudinit import importer +from cloudinit import log as logging +from cloudinit import url_helper as uhelp + +from cloudinit.settings import (CFG_BUILTIN) + + +LOG = logging.getLogger(__name__) + +# Helps cleanup filenames to ensure they aren't FS incompatible +FN_REPLACEMENTS = { + os.sep: '_', +} +FN_ALLOWED = ('_-.()' + string.digits + string.ascii_letters) + +# Helper utils to see if running in a container +CONTAINER_TESTS = ['running-in-container', 'lxc-is-container'] + + +class ProcessExecutionError(IOError): + + MESSAGE_TMPL = ('%(description)s\n' + 'Command: %(cmd)s\n' + 'Exit code: %(exit_code)s\n' + 'Reason: %(reason)s\n' + 'Stdout: %(stdout)r\n' + 'Stderr: %(stderr)r') + + def __init__(self, stdout=None, stderr=None, + exit_code=None, cmd=None, + description=None, reason=None): + if not cmd: + self.cmd = '-' + else: + self.cmd = cmd + + if not description: + self.description = 'Unexpected error while running command.' + else: + self.description = description + + if not isinstance(exit_code, (long, int)): + self.exit_code = '-' + else: + self.exit_code = exit_code + + if not stderr: + self.stderr = '' + else: + self.stderr = stderr + + if not stdout: + self.stdout = '' + else: + self.stdout = stdout + + if reason: + self.reason = reason + else: + self.reason = '-' + + message = self.MESSAGE_TMPL % { + 'description': self.description, + 'cmd': self.cmd, + 'exit_code': self.exit_code, + 'stdout': self.stdout, + 'stderr': self.stderr, + 'reason': self.reason, + } + IOError.__init__(self, message) + + +class SeLinuxGuard(object): + def __init__(self, path, recursive=False): + # Late import since it might not always + # be possible to use this + try: + self.selinux = importer.import_module('selinux') + except ImportError: + self.selinux = None + self.path = path + self.recursive = recursive + + def __enter__(self): + if self.selinux: + return True + else: + return False + + def __exit__(self, excp_type, excp_value, excp_traceback): + if self.selinux: + path = os.path.realpath(os.path.expanduser(self.path)) + do_restore = False + try: + # See if even worth restoring?? + stats = os.lstat(path) + if stat.ST_MODE in stats: + self.selinux.matchpathcon(path, stats[stat.ST_MODE]) + do_restore = True + except OSError: + pass + if do_restore: + LOG.debug("Restoring selinux mode for %s (recursive=%s)", + path, self.recursive) + self.selinux.restorecon(path, recursive=self.recursive) + + +class MountFailedError(Exception): + pass + + +def ExtendedTemporaryFile(**kwargs): + fh = tempfile.NamedTemporaryFile(**kwargs) + # Replace its unlink with a quiet version + # that does not raise errors when the + # file to unlink has been unlinked elsewhere.. + LOG.debug("Created temporary file %s", fh.name) + fh.unlink = del_file + + # Add a new method that will unlink + # right 'now' but still lets the exit + # method attempt to remove it (which will + # not throw due to our del file being quiet + # about files that are not there) + def unlink_now(): + fh.unlink(fh.name) + + setattr(fh, 'unlink_now', unlink_now) + return fh + + +def fork_cb(child_cb, *args): + fid = os.fork() + if fid == 0: + try: + child_cb(*args) + os._exit(0) # pylint: disable=W0212 + except: + logexc(LOG, ("Failed forking and" + " calling callback %s"), obj_name(child_cb)) + os._exit(1) # pylint: disable=W0212 + else: + LOG.debug("Forked child %s who will run callback %s", + fid, obj_name(child_cb)) + + +def is_true(val, addons=None): + if isinstance(val, (bool)): + return val is True + check_set = ['true', '1', 'on', 'yes'] + if addons: + check_set = check_set + addons + if str(val).lower().strip() in check_set: + return True + return False + + +def is_false(val, addons=None): + if isinstance(val, (bool)): + return val is False + check_set = ['off', '0', 'no', 'false'] + if addons: + check_set = check_set + addons + if str(val).lower().strip() in check_set: + return True + return False + + +def translate_bool(val, addons=None): + if not val: + # This handles empty lists and false and + # other things that python believes are false + return False + # If its already a boolean skip + if isinstance(val, (bool)): + return val + return is_true(val, addons) + + +def rand_str(strlen=32, select_from=None): + if not select_from: + select_from = string.letters + string.digits + return "".join([random.choice(select_from) for _x in range(0, strlen)]) def read_conf(fname): try: - stream = open(fname, "r") - conf = yaml.safe_load(stream) - stream.close() - return conf + return load_yaml(load_file(fname), default={}) except IOError as e: if e.errno == errno.ENOENT: return {} - raise - + else: + raise -def get_base_cfg(cfgfile, cfg_builtin="", parsed_cfgs=None): - kerncfg = {} - syscfg = {} - if parsed_cfgs and cfgfile in parsed_cfgs: - return(parsed_cfgs[cfgfile]) - syscfg = read_conf_with_confd(cfgfile) +def clean_filename(fn): + for (k, v) in FN_REPLACEMENTS.iteritems(): + fn = fn.replace(k, v) + removals = [] + for k in fn: + if k not in FN_ALLOWED: + removals.append(k) + for k in removals: + fn = fn.replace(k, '') + fn = fn.strip() + return fn - kern_contents = read_cc_from_cmdline() - if kern_contents: - kerncfg = yaml.safe_load(kern_contents) - # kernel parameters override system config - combined = mergedict(kerncfg, syscfg) +def decomp_str(data): + try: + buf = StringIO(str(data)) + with contextlib.closing(gzip.GzipFile(None, "rb", 1, buf)) as gh: + return gh.read() + except: + return data + + +def find_modules(root_dir): + entries = dict() + for fname in glob.glob(os.path.join(root_dir, "*.py")): + if not os.path.isfile(fname): + continue + modname = os.path.basename(fname)[0:-3] + modname = modname.strip() + if modname and modname.find(".") == -1: + entries[fname] = modname + return entries + + +def multi_log(text, console=True, stderr=True, + log=None, log_level=logging.DEBUG): + if stderr: + sys.stderr.write(text) + if console: + # Don't use the write_file since + # this might be 'sensitive' info (not debug worthy?) + with open('/dev/console', 'wb') as wfh: + wfh.write(text) + wfh.flush() + if log: + log.log(log_level, text) + + +def is_ipv4(instr): + """ determine if input string is a ipv4 address. return boolean""" + toks = instr.split('.') + if len(toks) != 4: + return False - if cfg_builtin: - builtin = yaml.safe_load(cfg_builtin) - fin = mergedict(combined, builtin) - else: - fin = combined + try: + toks = [x for x in toks if (int(x) < 256 and int(x) > 0)] + except: + return False - if parsed_cfgs != None: - parsed_cfgs[cfgfile] = fin - return(fin) + return (len(toks) == 4) def get_cfg_option_bool(yobj, key, default=False): if key not in yobj: return default - val = yobj[key] - if val is True: - return True - if str(val).lower() in ['true', '1', 'on', 'yes']: - return True - return False + return translate_bool(yobj[key]) def get_cfg_option_str(yobj, key, default=None): if key not in yobj: return default - return yobj[key] + val = yobj[key] + if not isinstance(val, (str, basestring)): + val = str(val) + return val -def get_cfg_option_list_or_str(yobj, key, default=None): +def system_info(): + return { + 'platform': platform.platform(), + 'release': platform.release(), + 'python': platform.python_version(), + 'uname': platform.uname(), + } + + +def get_cfg_option_list(yobj, key, default=None): """ Gets the C{key} config option from C{yobj} as a list of strings. If the key is present as a single string it will be returned as a list with one @@ -114,9 +343,14 @@ def get_cfg_option_list_or_str(yobj, key, default=None): return default if yobj[key] is None: return [] - if isinstance(yobj[key], list): - return yobj[key] - return [yobj[key]] + val = yobj[key] + if isinstance(val, (list)): + # Should we ensure they are all strings?? + cval = [str(v) for v in val] + return cval + if not isinstance(val, (str, basestring)): + val = str(val) + return [val] # get a cfg entry by its path array @@ -125,18 +359,121 @@ def get_cfg_by_path(yobj, keyp, default=None): cur = yobj for tok in keyp: if tok not in cur: - return(default) + return default cur = cur[tok] - return(cur) + return cur + + +def fixup_output(cfg, mode): + (outfmt, errfmt) = get_output_cfg(cfg, mode) + redirect_output(outfmt, errfmt) + return (outfmt, errfmt) + + +# redirect_output(outfmt, errfmt, orig_out, orig_err) +# replace orig_out and orig_err with filehandles specified in outfmt or errfmt +# fmt can be: +# > FILEPATH +# >> FILEPATH +# | program [ arg1 [ arg2 [ ... ] ] ] +# +# with a '|', arguments are passed to shell, so one level of +# shell escape is required. +def redirect_output(outfmt, errfmt, o_out=None, o_err=None): + if not o_out: + o_out = sys.stdout + if not o_err: + o_err = sys.stderr + + if outfmt: + LOG.debug("Redirecting %s to %s", o_out, outfmt) + (mode, arg) = outfmt.split(" ", 1) + if mode == ">" or mode == ">>": + owith = "ab" + if mode == ">": + owith = "wb" + new_fp = open(arg, owith) + elif mode == "|": + proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE) + new_fp = proc.stdin + else: + raise TypeError("Invalid type for output format: %s" % outfmt) + + if o_out: + os.dup2(new_fp.fileno(), o_out.fileno()) + + if errfmt == outfmt: + LOG.debug("Redirecting %s to %s", o_err, outfmt) + os.dup2(new_fp.fileno(), o_err.fileno()) + return + + if errfmt: + LOG.debug("Redirecting %s to %s", o_err, errfmt) + (mode, arg) = errfmt.split(" ", 1) + if mode == ">" or mode == ">>": + owith = "ab" + if mode == ">": + owith = "wb" + new_fp = open(arg, owith) + elif mode == "|": + proc = subprocess.Popen(arg, shell=True, stdin=subprocess.PIPE) + new_fp = proc.stdin + else: + raise TypeError("Invalid type for error format: %s" % errfmt) + + if o_err: + os.dup2(new_fp.fileno(), o_err.fileno()) + + +def make_url(scheme, host, port=None, + path='', params='', query='', fragment=''): + + pieces = [] + pieces.append(scheme or '') + + netloc = '' + if host: + netloc = str(host) + + if port is not None: + netloc += ":" + "%s" % (port) + + pieces.append(netloc or '') + pieces.append(path or '') + pieces.append(params or '') + pieces.append(query or '') + pieces.append(fragment or '') + + return urlparse.urlunparse(pieces) + + +def obj_name(obj): + if isinstance(obj, (types.TypeType, + types.ModuleType, + types.FunctionType, + types.LambdaType)): + return str(obj.__name__) + return obj_name(obj.__class__) + + +def mergemanydict(srcs, reverse=False): + if reverse: + srcs = reversed(srcs) + m_cfg = {} + for a_cfg in srcs: + if a_cfg: + m_cfg = mergedict(m_cfg, a_cfg) + return m_cfg def mergedict(src, cand): """ - Merge values from C{cand} into C{src}. If C{src} has a key C{cand} will - not override. Nested dictionaries are merged recursively. + Merge values from C{cand} into C{src}. + If C{src} has a key C{cand} will not override. + Nested dictionaries are merged recursively. """ if isinstance(src, dict) and isinstance(cand, dict): - for k, v in cand.iteritems(): + for (k, v) in cand.iteritems(): if k not in src: src[k] = v else: @@ -144,104 +481,66 @@ def mergedict(src, cand): return src -def delete_dir_contents(dirname): - """ - Deletes all contents of a directory without deleting the directory itself. +@contextlib.contextmanager +def chdir(ndir): + curr = os.getcwd() + try: + os.chdir(ndir) + yield ndir + finally: + os.chdir(curr) - @param dirname: The directory whose contents should be deleted. - """ - for node in os.listdir(dirname): - node_fullpath = os.path.join(dirname, node) - if os.path.isdir(node_fullpath): - shutil.rmtree(node_fullpath) - else: - os.unlink(node_fullpath) +@contextlib.contextmanager +def umask(n_msk): + old = os.umask(n_msk) + try: + yield old + finally: + os.umask(old) -def write_file(filename, content, mode=0644, omode="wb"): - """ - Writes a file with the given content and sets the file mode as specified. - Resotres the SELinux context if possible. - @param filename: The full path of the file to write. - @param content: The content to write to the file. - @param mode: The filesystem mode to set on the file. - @param omode: The open mode used when opening the file (r, rb, a, etc.) - """ +@contextlib.contextmanager +def tempdir(**kwargs): + # This seems like it was only added in python 3.2 + # Make it since its useful... + # See: http://bugs.python.org/file12970/tempdir.patch + tdir = tempfile.mkdtemp(**kwargs) try: - os.makedirs(os.path.dirname(filename)) - except OSError as e: - if e.errno != errno.EEXIST: - raise e + yield tdir + finally: + del_dir(tdir) - f = open(filename, omode) - if mode is not None: - os.chmod(filename, mode) - f.write(content) - f.close() - restorecon_if_possible(filename) - - -def restorecon_if_possible(path, recursive=False): - if HAVE_LIBSELINUX and selinux.is_selinux_enabled(): - selinux.restorecon(path, recursive=recursive) - - -# get keyid from keyserver -def getkeybyid(keyid, keyserver): - shcmd = """ - k=${1} ks=${2}; - exec 2>/dev/null - [ -n "$k" ] || exit 1; - armour=$(gpg --list-keys --armour "${k}") - if [ -z "${armour}" ]; then - gpg --keyserver ${ks} --recv $k >/dev/null && - armour=$(gpg --export --armour "${k}") && - gpg --batch --yes --delete-keys "${k}" - fi - [ -n "${armour}" ] && echo "${armour}" - """ - args = ['sh', '-c', shcmd, "export-gpg-keyid", keyid, keyserver] - return(subp(args)[0]) + +def center(text, fill, max_len): + return '{0:{fill}{align}{size}}'.format(text, fill=fill, + align="^", size=max_len) + + +def del_dir(path): + LOG.debug("Recursively deleting %s", path) + shutil.rmtree(path) def runparts(dirp, skip_no_exist=True): if skip_no_exist and not os.path.isdir(dirp): return - failed = 0 + failed = [] + attempted = [] for exe_name in sorted(os.listdir(dirp)): exe_path = os.path.join(dirp, exe_name) if os.path.isfile(exe_path) and os.access(exe_path, os.X_OK): - popen = subprocess.Popen([exe_path]) - popen.communicate() - if popen.returncode is not 0: - failed += 1 - sys.stderr.write("failed: %s [%i]\n" % - (exe_path, popen.returncode)) - if failed: - raise RuntimeError('runparts: %i failures' % failed) - - -def subp(args, input_=None): - sp = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, stdin=subprocess.PIPE) - out, err = sp.communicate(input_) - if sp.returncode is not 0: - raise subprocess.CalledProcessError(sp.returncode, args, (out, err)) - return(out, err) - - -def render_to_file(template, outfile, searchList): - t = Template(file='/etc/cloud/templates/%s.tmpl' % template, - searchList=[searchList]) - f = open(outfile, 'w') - f.write(t.respond()) - f.close() - + attempted.append(exe_path) + try: + subp([exe_path]) + except ProcessExecutionError as e: + logexc(LOG, "Failed running %s [%s]", exe_path, e.exit_code) + failed.append(e) -def render_string(template, searchList): - return(Template(template, searchList=[searchList]).respond()) + if failed and attempted: + raise RuntimeError('Runparts: %s failures in %s attempted commands' + % (len(failed), len(attempted))) # read_optional_seed @@ -254,13 +553,39 @@ def read_optional_seed(fill, base="", ext="", timeout=5): fill['user-data'] = ud fill['meta-data'] = md return True - except OSError, e: + except OSError as e: if e.errno == errno.ENOENT: return False raise -# raise OSError with enoent if not found +def read_file_or_url(url, timeout=5, retries=10, file_retries=0): + if url.startswith("/"): + url = "file://%s" % url + if url.startswith("file://"): + retries = file_retries + return uhelp.readurl(url, timeout=timeout, retries=retries) + + +def load_yaml(blob, default=None, allowed=(dict,)): + loaded = default + try: + blob = str(blob) + LOG.debug(("Attempting to load yaml from string " + "of length %s with allowed root types %s"), + len(blob), allowed) + converted = yaml.safe_load(blob) + if not isinstance(converted, allowed): + # Yes this will just be caught, but thats ok for now... + raise TypeError(("Yaml load allows %s root types," + " but got %s instead") % + (allowed, obj_name(converted))) + loaded = converted + except (yaml.YAMLError, TypeError, ValueError): + logexc(LOG, "Failed loading yaml blob") + return loaded + + def read_seeded(base="", ext="", timeout=5, retries=10, file_retries=0): if base.startswith("/"): base = "file://%s" % base @@ -276,139 +601,62 @@ def read_seeded(base="", ext="", timeout=5, retries=10, file_retries=0): ud_url = "%s%s%s" % (base, "user-data", ext) md_url = "%s%s%s" % (base, "meta-data", ext) - no_exc = object() - raise_err = no_exc - for attempt in range(0, retries + 1): - try: - md_str = readurl(md_url, timeout=timeout) - ud = readurl(ud_url, timeout=timeout) - md = yaml.safe_load(md_str) - - return(md, ud) - except urllib2.HTTPError as e: - raise_err = e - except urllib2.URLError as e: - raise_err = e - if (isinstance(e.reason, OSError) and - e.reason.errno == errno.ENOENT): - raise_err = e.reason - - if attempt == retries: - break - - #print "%s failed, sleeping" % attempt - time.sleep(1) - - raise(raise_err) - + md_resp = read_file_or_url(md_url, timeout, retries, file_retries) + md = None + if md_resp.ok(): + md_str = str(md_resp) + md = load_yaml(md_str, default={}) -def logexc(log, lvl=logging.DEBUG): - log.log(lvl, traceback.format_exc()) + ud_resp = read_file_or_url(ud_url, timeout, retries, file_retries) + ud = None + if ud_resp.ok(): + ud_str = str(ud_resp) + ud = ud_str - -class RecursiveInclude(Exception): - pass - - -def read_file_with_includes(fname, rel=".", stack=None, patt=None): - if stack is None: - stack = [] - if not fname.startswith("/"): - fname = os.sep.join((rel, fname)) - - fname = os.path.realpath(fname) - - if fname in stack: - raise(RecursiveInclude("%s recursively included" % fname)) - if len(stack) > 10: - raise(RecursiveInclude("%s included, stack size = %i" % - (fname, len(stack)))) - - if patt == None: - patt = re.compile("^#(opt_include|include)[ \t].*$", re.MULTILINE) - - try: - fp = open(fname) - contents = fp.read() - fp.close() - except: - raise - - rel = os.path.dirname(fname) - stack.append(fname) - - cur = 0 - while True: - match = patt.search(contents[cur:]) - if not match: - break - loc = match.start() + cur - endl = match.end() + cur - - (key, cur_fname) = contents[loc:endl].split(None, 2) - cur_fname = cur_fname.strip() - - try: - inc_contents = read_file_with_includes(cur_fname, rel, stack, patt) - except IOError, e: - if e.errno == errno.ENOENT and key == "#opt_include": - inc_contents = "" - else: - raise - contents = contents[0:loc] + inc_contents + contents[endl + 1:] - cur = loc + len(inc_contents) - stack.pop() - return(contents) + return (md, ud) def read_conf_d(confd): - # get reverse sorted list (later trumps newer) + # Get reverse sorted list (later trumps newer) confs = sorted(os.listdir(confd), reverse=True) - # remove anything not ending in '.cfg' + # Remove anything not ending in '.cfg' confs = [f for f in confs if f.endswith(".cfg")] - # remove anything not a file - confs = [f for f in confs if os.path.isfile("%s/%s" % (confd, f))] + # Remove anything not a file + confs = [f for f in confs + if os.path.isfile(os.path.join(confd, f))] - cfg = {} - for conf in confs: - cfg = mergedict(cfg, read_conf("%s/%s" % (confd, conf))) + # Load them all so that they can be merged + cfgs = [] + for fn in confs: + cfgs.append(read_conf(os.path.join(confd, fn))) - return(cfg) + return mergemanydict(cfgs) def read_conf_with_confd(cfgfile): cfg = read_conf(cfgfile) + confd = False if "conf_d" in cfg: - if cfg['conf_d'] is not None: - confd = cfg['conf_d'] - if not isinstance(confd, str): - raise Exception("cfgfile %s contains 'conf_d' " - "with non-string" % cfgfile) + confd = cfg['conf_d'] + if confd: + if not isinstance(confd, (str, basestring)): + raise TypeError(("Config file %s contains 'conf_d' " + "with non-string type %s") % + (cfgfile, obj_name(confd))) + else: + confd = str(confd).strip() elif os.path.isdir("%s.d" % cfgfile): confd = "%s.d" % cfgfile - if not confd: - return(cfg) + if not confd or not os.path.isdir(confd): + return cfg + # Conf.d settings override input configuration confd_cfg = read_conf_d(confd) - - return(mergedict(confd_cfg, cfg)) - - -def get_cmdline(): - if 'DEBUG_PROC_CMDLINE' in os.environ: - cmdline = os.environ["DEBUG_PROC_CMDLINE"] - else: - try: - cmdfp = open("/proc/cmdline") - cmdline = cmdfp.read().strip() - cmdfp.close() - except: - cmdline = "" - return(cmdline) + return mergedict(confd_cfg, cfg) def read_cc_from_cmdline(cmdline=None): @@ -439,147 +687,15 @@ def read_cc_from_cmdline(cmdline=None): begin = cmdline.find(tag_begin, end + end_l) - return('\n'.join(tokens)) - - -def ensure_dirs(dirlist, mode=0755): - fixmodes = [] - for d in dirlist: - try: - if mode != None: - os.makedirs(d) - else: - os.makedirs(d, mode) - except OSError as e: - if e.errno != errno.EEXIST: - raise - if mode != None: - fixmodes.append(d) - - for d in fixmodes: - os.chmod(d, mode) - - -def chownbyname(fname, user=None, group=None): - uid = -1 - gid = -1 - if user == None and group == None: - return - if user: - import pwd - uid = pwd.getpwnam(user).pw_uid - if group: - import grp - gid = grp.getgrnam(group).gr_gid - - os.chown(fname, uid, gid) - + return '\n'.join(tokens) -def readurl(url, data=None, timeout=None): - openargs = {} - if timeout != None: - openargs['timeout'] = timeout - if data is None: - req = urllib2.Request(url) - else: - encoded = urllib.urlencode(data) - req = urllib2.Request(url, encoded) - - response = urllib2.urlopen(req, **openargs) - return(response.read()) - - -# shellify, takes a list of commands -# for each entry in the list -# if it is an array, shell protect it (with single ticks) -# if it is a string, do nothing -def shellify(cmdlist): - content = "#!/bin/sh\n" - escaped = "%s%s%s%s" % ("'", '\\', "'", "'") - for args in cmdlist: - # if the item is a list, wrap all items in single tick - # if its not, then just write it directly - if isinstance(args, list): - fixed = [] - for f in args: - fixed.append("'%s'" % str(f).replace("'", escaped)) - content = "%s%s\n" % (content, ' '.join(fixed)) - else: - content = "%s%s\n" % (content, str(args)) - return content - - -def dos2unix(string): +def dos2unix(contents): # find first end of line - pos = string.find('\n') - if pos <= 0 or string[pos - 1] != '\r': - return(string) - return(string.replace('\r\n', '\n')) - - -def is_container(): - # is this code running in a container of some sort - - for helper in ('running-in-container', 'lxc-is-container'): - try: - # try to run a helper program. if it returns true - # then we're inside a container. otherwise, no - sp = subprocess.Popen(helper, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - sp.communicate(None) - return(sp.returncode == 0) - except OSError as e: - if e.errno != errno.ENOENT: - raise - - # this code is largely from the logic in - # ubuntu's /etc/init/container-detect.conf - try: - # Detect old-style libvirt - # Detect OpenVZ containers - pid1env = get_proc_env(1) - if "container" in pid1env: - return True - - if "LIBVIRT_LXC_UUID" in pid1env: - return True - - except IOError as e: - if e.errno != errno.ENOENT: - pass - - # Detect OpenVZ containers - if os.path.isdir("/proc/vz") and not os.path.isdir("/proc/bc"): - return True - - try: - # Detect Vserver containers - with open("/proc/self/status") as fp: - lines = fp.read().splitlines() - for line in lines: - if line.startswith("VxID:"): - (_key, val) = line.strip().split(":", 1) - if val != "0": - return True - except IOError as e: - if e.errno != errno.ENOENT: - pass - - return False - - -def get_proc_env(pid): - # return the environment in a dict that a given process id was started with - env = {} - with open("/proc/%s/environ" % pid) as fp: - toks = fp.read().split("\0") - for tok in toks: - if tok == "": - continue - (name, val) = tok.split("=", 1) - env[name] = val - return env + pos = contents.find('\n') + if pos <= 0 or contents[pos - 1] != '\r': + return contents + return contents.replace('\r\n', '\n') def get_hostname_fqdn(cfg, cloud): @@ -603,38 +719,72 @@ def get_hostname_fqdn(cfg, cloud): hostname = cfg['hostname'] else: hostname = cloud.get_hostname() - return(hostname, fqdn) + return (hostname, fqdn) def get_fqdn_from_hosts(hostname, filename="/etc/hosts"): - # this parses /etc/hosts to get a fqdn. It should return the same - # result as 'hostname -f <hostname>' if /etc/hosts.conf - # did not have did not have 'bind' in the order attribute + """ + For each host a single line should be present with + the following information: + + IP_address canonical_hostname [aliases...] + + Fields of the entry are separated by any number of blanks and/or tab + characters. Text from a "#" character until the end of the line is a + comment, and is ignored. Host names may contain only alphanumeric + characters, minus signs ("-"), and periods ("."). They must begin with + an alphabetic character and end with an alphanumeric character. + Optional aliases provide for name changes, alternate spellings, shorter + hostnames, or generic hostnames (for example, localhost). + """ fqdn = None try: - with open(filename, "r") as hfp: - for line in hfp.readlines(): - hashpos = line.find("#") - if hashpos >= 0: - line = line[0:hashpos] - toks = line.split() - - # if there there is less than 3 entries (ip, canonical, alias) - # then ignore this line - if len(toks) < 3: - continue - - if hostname in toks[2:]: - fqdn = toks[1] - break - hfp.close() - except IOError as e: - if e.errno == errno.ENOENT: - pass + for line in load_file(filename).splitlines(): + hashpos = line.find("#") + if hashpos >= 0: + line = line[0:hashpos] + line = line.strip() + if not line: + continue + # If there there is less than 3 entries + # (IP_address, canonical_hostname, alias) + # then ignore this line + toks = line.split() + if len(toks) < 3: + continue + + if hostname in toks[2:]: + fqdn = toks[1] + break + except IOError: + pass return fqdn +def get_cmdline_url(names=('cloud-config-url', 'url'), + starts="#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 = uhelp.readurl(url) + if resp.contents.startswith(starts) and resp.ok(): + return (key, url, str(resp)) + + return (key, url, None) + + def is_resolvable(name): """ determine if a url is resolvable, return a boolean """ try: @@ -644,9 +794,14 @@ def is_resolvable(name): return False +def get_hostname(): + hostname = socket.gethostname() + return hostname + + def is_resolvable_url(url): """ determine if this url is resolvable (existing or ip) """ - return(is_resolvable(urlparse.urlparse(url).hostname)) + return (is_resolvable(urlparse.urlparse(url).hostname)) def search_for_mirror(candidates): @@ -656,8 +811,7 @@ def search_for_mirror(candidates): if is_resolvable_url(cand): return cand except Exception: - raise - + pass return None @@ -669,13 +823,14 @@ def close_stdin(): 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 os.environ.get("_CLOUD_INIT_SAVE_STDIN") in ("", "0", False): + if os.environ.get("_CLOUD_INIT_SAVE_STDIN") in ("", "0", 'False'): return with open(os.devnull) as fp: os.dup2(fp.fileno(), sys.stdin.fileno()) -def find_devs_with(criteria): +def find_devs_with(criteria=None, oformat='device', + tag=None, no_cache=False, path=None): """ find devices matching given criteria (via blkid) criteria can be *one* of: @@ -683,165 +838,555 @@ def find_devs_with(criteria): LABEL=<label> UUID=<uuid> """ + blk_id_cmd = ['blkid'] + options = [] + if criteria: + # Search for block devices with tokens named NAME that + # have the value 'value' and display any devices which are found. + # Common values for NAME include TYPE, LABEL, and UUID. + # If there are no devices specified on the command line, + # all block devices will be searched; otherwise, + # only search the devices specified by the user. + options.append("-t%s" % (criteria)) + if tag: + # For each (specified) device, show only the tags that match tag. + options.append("-s%s" % (tag)) + if no_cache: + # If you want to start with a clean cache + # (i.e. don't report devices previously scanned + # but not necessarily available at this time), specify /dev/null. + options.extend(["-c", "/dev/null"]) + if oformat: + # Display blkid's output using the specified format. + # The format parameter may be: + # full, value, list, device, udev, export + options.append('-o%s' % (oformat)) + if path: + options.append(path) + cmd = blk_id_cmd + options + # See man blkid for why 2 is added + (out, _err) = subp(cmd, rcs=[0, 2]) + entries = [] + for line in out.splitlines(): + line = line.strip() + if line: + entries.append(line) + return entries + + +def load_file(fname, read_cb=None, quiet=False): + LOG.debug("Reading from %s (quiet=%s)", fname, quiet) + ofh = StringIO() try: - (out, _err) = subp(['blkid', '-t%s' % criteria, '-odevice']) - except subprocess.CalledProcessError: - return([]) - return(str(out).splitlines()) + with open(fname, 'rb') as ifh: + pipe_in_out(ifh, ofh, chunk_cb=read_cb) + except IOError as e: + if not quiet: + raise + if e.errno != errno.ENOENT: + raise + contents = ofh.getvalue() + LOG.debug("Read %s bytes from %s", len(contents), fname) + return contents -class mountFailedError(Exception): - pass +def get_cmdline(): + if 'DEBUG_PROC_CMDLINE' in os.environ: + cmdline = os.environ["DEBUG_PROC_CMDLINE"] + else: + try: + cmdline = load_file("/proc/cmdline").strip() + except: + cmdline = "" + return cmdline -def mount_callback_umount(device, callback, data=None): +def pipe_in_out(in_fh, out_fh, chunk_size=1024, chunk_cb=None): + bytes_piped = 0 + while True: + data = in_fh.read(chunk_size) + if data == '': + break + else: + out_fh.write(data) + bytes_piped += len(data) + if chunk_cb: + chunk_cb(bytes_piped) + out_fh.flush() + return bytes_piped + + +def chownbyid(fname, uid=None, gid=None): + if uid in [None, -1] and gid in [None, -1]: + # Nothing to do + return + LOG.debug("Changing the ownership of %s to %s:%s", fname, uid, gid) + os.chown(fname, uid, gid) + + +def chownbyname(fname, user=None, group=None): + uid = -1 + gid = -1 + try: + if user: + 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 + chownbyid(fname, uid, gid) + return True + + +# Always returns well formated values +# cfg is expected to have an entry 'output' in it, which is a dictionary +# that includes entries for 'init', 'config', 'final' or 'all' +# init: /var/log/cloud.out +# config: [ ">> /var/log/cloud-config.out", /var/log/cloud-config.err ] +# final: +# output: "| logger -p" +# error: "> /dev/null" +# this returns the specific 'mode' entry, cleanly formatted, with value +def get_output_cfg(cfg, mode): + ret = [None, None] + if not cfg or not 'output' in cfg: + return ret + + outcfg = cfg['output'] + if mode in outcfg: + modecfg = outcfg[mode] + else: + if 'all' not in outcfg: + return ret + # if there is a 'all' item in the output list + # then it applies to all users of this (init, config, final) + modecfg = outcfg['all'] + + # if value is a string, it specifies stdout and stderr + if isinstance(modecfg, str): + ret = [modecfg, modecfg] + + # if its a list, then we expect (stdout, stderr) + if isinstance(modecfg, list): + if len(modecfg) > 0: + ret[0] = modecfg[0] + if len(modecfg) > 1: + ret[1] = modecfg[1] + + # if it is a dictionary, expect 'out' and 'error' + # items, which indicate out and error + if isinstance(modecfg, dict): + if 'output' in modecfg: + ret[0] = modecfg['output'] + if 'error' in modecfg: + ret[1] = modecfg['error'] + + # if err's entry == "&1", then make it same as stdout + # as in shell syntax of "echo foo >/dev/null 2>&1" + if ret[1] == "&1": + ret[1] = ret[0] + + swlist = [">>", ">", "|"] + for i in range(len(ret)): + if not ret[i]: + continue + val = ret[i].lstrip() + found = False + for s in swlist: + if val.startswith(s): + val = "%s %s" % (s, val[len(s):].strip()) + found = True + break + if not found: + # default behavior is append + val = "%s %s" % (">>", val.strip()) + ret[i] = val + + return ret + + +def logexc(log, msg, *args): + # Setting this here allows this to change + # levels easily (not always error level) + # or even desirable to have that much junk + # coming out to a non-debug stream + if msg: + log.warn(msg, *args) + # Debug gets the full trace + log.debug(msg, exc_info=1, *args) + + +def hash_blob(blob, routine, mlen=None): + hasher = hashlib.new(routine) + hasher.update(blob) + digest = hasher.hexdigest() + # Don't get to long now + if mlen is not None: + return digest[0:mlen] + else: + return digest + + +def rename(src, dest): + LOG.debug("Renaming %s to %s", src, dest) + # TODO use a se guard here?? + os.rename(src, dest) + + +def ensure_dirs(dirlist, mode=0755): + for d in dirlist: + 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: + logexc(LOG, "Failed fetching command line url") + return + try: + if key and content: + write_file(target_fn, content, mode=0600) + 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: + logexc(LOG, "Failed writing url content to %s", target_fn) + + +def yaml_dumps(obj): + formatted = yaml.dump(obj, + line_break="\n", + indent=4, + explicit_start=True, + explicit_end=True, + default_flow_style=False, + ) + return formatted + + +def ensure_dir(path, mode=None): + if not os.path.isdir(path): + # Make the dir and adjust the mode + with SeLinuxGuard(os.path.dirname(path), recursive=True): + os.makedirs(path) + chmod(path, mode) + else: + # Just adjust the mode + chmod(path, mode) + + +@contextlib.contextmanager +def unmounter(umount): + try: + yield umount + finally: + if umount: + umount_cmd = ["umount", '-l', umount] + subp(umount_cmd) + + +def mounts(): + mounted = {} + try: + # Go through mounts to see what is already mounted + mount_locs = load_file("/proc/mounts").splitlines() + for mpline in mount_locs: + # Format at: man fstab + try: + (dev, mp, fstype, opts, _freq, _passno) = mpline.split() + except: + continue + # If the name of the mount point contains spaces these + # can be escaped as '\040', so undo that.. + mp = mp.replace("\\040", " ") + mounted[dev] = { + 'fstype': fstype, + 'mountpoint': mp, + 'opts': opts, + } + LOG.debug("Fetched %s mounts from %s", mounted, "/proc/mounts") + except (IOError, OSError): + logexc(LOG, "Failed fetching mount points from /proc/mounts") + return mounted + + +def mount_cb(device, callback, data=None, rw=False, mtype=None, sync=True): """ - mount the device, call method 'callback' passing the directory + Mount the device, call method 'callback' passing the directory in which it was mounted, then unmount. Return whatever 'callback' returned. If data != None, also pass data to callback. """ - - def _cleanup(umount, tmpd): - if umount: + mounted = mounts() + with tempdir() as tmpd: + umount = False + if device in mounted: + mountpoint = mounted[device]['mountpoint'] + else: try: - subp(["umount", '-l', umount]) - except subprocess.CalledProcessError: - raise - if tmpd: - os.rmdir(tmpd) + mountcmd = ['mount'] + mountopts = [] + if rw: + mountopts.append('rw') + else: + mountopts.append('ro') + if sync: + # This seems like the safe approach to do + # (ie where this is on by default) + mountopts.append("sync") + if mountopts: + mountcmd.extend(["-o", ",".join(mountopts)]) + if mtype: + mountcmd.extend(['-t', mtype]) + mountcmd.append(device) + mountcmd.append(tmpd) + subp(mountcmd) + umount = tmpd # This forces it to be unmounted (when set) + mountpoint = tmpd + except (IOError, OSError) as exc: + raise MountFailedError(("Failed mounting %s " + "to %s due to: %s") % + (device, tmpd, exc)) + # Be nice and ensure it ends with a slash + if not mountpoint.endswith("/"): + mountpoint += "/" + with unmounter(umount): + if data is None: + ret = callback(mountpoint) + else: + ret = callback(mountpoint, data) + return ret - # go through mounts to see if it was already mounted - fp = open("/proc/mounts") - mounts = fp.readlines() - fp.close() - tmpd = None +def get_builtin_cfg(): + # Deep copy so that others can't modify + return obj_copy.deepcopy(CFG_BUILTIN) - mounted = {} - for mpline in mounts: - (dev, mp, fstype, _opts, _freq, _passno) = mpline.split() - mp = mp.replace("\\040", " ") - mounted[dev] = (dev, fstype, mp, False) - - umount = False - if device in mounted: - mountpoint = "%s/" % mounted[device][2] - else: - tmpd = tempfile.mkdtemp() - mountcmd = ["mount", "-o", "ro", device, tmpd] +def sym_link(source, link): + LOG.debug("Creating symbolic link from %r => %r" % (link, source)) + os.symlink(source, link) - try: - (_out, _err) = subp(mountcmd) - umount = tmpd - except subprocess.CalledProcessError as exc: - _cleanup(umount, tmpd) - raise mountFailedError(exc.output[1]) - mountpoint = "%s/" % tmpd +def del_file(path): + LOG.debug("Attempting to remove %s", path) + try: + os.unlink(path) + except OSError as e: + if e.errno != errno.ENOENT: + raise e + +def copy(src, dest): + LOG.debug("Copying %s to %s", src, dest) + shutil.copy(src, dest) + + +def time_rfc2822(): try: - if data == None: - ret = callback(mountpoint) - else: - ret = callback(mountpoint, data) + ts = time.strftime("%a, %d %b %Y %H:%M:%S %z", time.gmtime()) + except: + ts = "??" + return ts - except Exception as exc: - _cleanup(umount, tmpd) - raise exc - _cleanup(umount, tmpd) +def uptime(): + uptime_str = '??' + try: + contents = load_file("/proc/uptime").strip() + if contents: + uptime_str = contents.split()[0] + except: + logexc(LOG, "Unable to read uptime from /proc/uptime") + return uptime_str + - return(ret) +def ensure_file(path, mode=0644): + write_file(path, content='', omode="ab", mode=mode) -def wait_for_url(urls, max_wait=None, timeout=None, - status_cb=None, headers_cb=None): +def chmod(path, mode): + real_mode = None + try: + real_mode = int(mode) + except (ValueError, TypeError): + pass + if path and real_mode: + with SeLinuxGuard(path): + os.chmod(path, real_mode) + + +def write_file(filename, content, mode=0644, omode="wb"): """ - urls: a list of urls to try - max_wait: roughly the maximum time to wait before giving up - The max time is *actually* len(urls)*timeout as each url will - be tried once and given the timeout provided. - timeout: the timeout provided to urllib2.urlopen - status_cb: call method with string message when a url is not available - headers_cb: call method with single argument of url to get headers - for request. - - the idea of this routine is to wait for the EC2 metdata service to - come up. On both Eucalyptus and EC2 we have seen the case where - the instance hit the MD before the MD service was up. EC2 seems - to have permenantely fixed this, though. - - In openstack, the metadata service might be painfully slow, and - unable to avoid hitting a timeout of even up to 10 seconds or more - (LP: #894279) for a simple GET. - - Offset those needs with the need to not hang forever (and block boot) - on a system where cloud-init is configured to look for EC2 Metadata - service but is not going to find one. It is possible that the instance - data host (169.254.169.254) may be firewalled off Entirely for a sytem, - meaning that the connection will block forever unless a timeout is set. + Writes a file with the given content and sets the file mode as specified. + Resotres the SELinux context if possible. + + @param filename: The full path of the file to write. + @param content: The content to write to the file. + @param mode: The filesystem mode to set on the file. + @param omode: The open mode used when opening the file (r, rb, a, etc.) """ - starttime = time.time() + ensure_dir(os.path.dirname(filename)) + LOG.debug("Writing to %s - %s: [%s] %s bytes", + filename, omode, mode, len(content)) + with SeLinuxGuard(path=filename): + with open(filename, omode) as fh: + fh.write(content) + fh.flush() + chmod(filename, mode) - sleeptime = 1 - def nullstatus_cb(msg): - return +def delete_dir_contents(dirname): + """ + Deletes all contents of a directory without deleting the directory itself. + + @param dirname: The directory whose contents should be deleted. + """ + for node in os.listdir(dirname): + node_fullpath = os.path.join(dirname, node) + if os.path.isdir(node_fullpath): + del_dir(node_fullpath) + else: + del_file(node_fullpath) - if status_cb == None: - status_cb = nullstatus_cb - def timeup(max_wait, starttime): - return((max_wait <= 0 or max_wait == None) or - (time.time() - starttime > max_wait)) +def subp(args, data=None, rcs=None, env=None, capture=True, shell=False): + if rcs is None: + rcs = [0] + try: + LOG.debug(("Running command %s with allowed return codes %s" + " (shell=%s, capture=%s)"), args, rcs, shell, capture) + if not capture: + stdout = None + stderr = None + else: + stdout = subprocess.PIPE + stderr = subprocess.PIPE + stdin = subprocess.PIPE + sp = subprocess.Popen(args, stdout=stdout, + stderr=stderr, stdin=stdin, + env=env, shell=shell) + (out, err) = sp.communicate(data) + except OSError as e: + raise ProcessExecutionError(cmd=args, reason=e) + rc = sp.returncode + if rc not in rcs: + raise ProcessExecutionError(stdout=out, stderr=err, + exit_code=rc, + cmd=args) + # Just ensure blank instead of none?? (iff capturing) + if not out and capture: + out = '' + if not err and capture: + err = '' + return (out, err) - loop_n = 0 - while True: - sleeptime = int(loop_n / 5) + 1 - for url in urls: - now = time.time() - if loop_n != 0: - if timeup(max_wait, starttime): - break - if timeout and (now + timeout > (starttime + max_wait)): - # shorten timeout to not run way over max_time - timeout = int((starttime + max_wait) - now) - - reason = "" - try: - if headers_cb != None: - headers = headers_cb(url) - else: - headers = {} - - req = urllib2.Request(url, data=None, headers=headers) - resp = urllib2.urlopen(req, timeout=timeout) - if resp.read() != "": - return url - reason = "empty data [%s]" % resp.getcode() - except urllib2.HTTPError as e: - reason = "http error [%s]" % e.code - except urllib2.URLError as e: - reason = "url error [%s]" % e.reason - except socket.timeout as e: - reason = "socket timeout [%s]" % e - except Exception as e: - reason = "unexpected error [%s]" % e - - status_cb("'%s' failed [%s/%ss]: %s" % - (url, int(time.time() - starttime), max_wait, - reason)) - - if timeup(max_wait, starttime): - break - loop_n = loop_n + 1 - time.sleep(sleeptime) +def abs_join(*paths): + return os.path.abspath(os.path.join(*paths)) + + +# shellify, takes a list of commands +# for each entry in the list +# if it is an array, shell protect it (with single ticks) +# if it is a string, do nothing +def shellify(cmdlist, add_header=True): + content = '' + if add_header: + content += "#!/bin/sh\n" + escaped = "%s%s%s%s" % ("'", '\\', "'", "'") + cmds_made = 0 + for args in cmdlist: + # If the item is a list, wrap all items in single tick. + # If its not, then just write it directly. + if isinstance(args, list): + fixed = [] + for f in args: + fixed.append("'%s'" % (str(f).replace("'", escaped))) + content = "%s%s\n" % (content, ' '.join(fixed)) + cmds_made += 1 + elif isinstance(args, (str, basestring)): + content = "%s%s\n" % (content, args) + cmds_made += 1 + else: + raise RuntimeError(("Unable to shellify type %s" + " which is not a list or string") + % (obj_name(args))) + LOG.debug("Shellified %s commands.", cmds_made) + return content + + +def is_container(): + """ + Checks to see if this code running in a container of some sort + """ + + for helper in CONTAINER_TESTS: + try: + # try to run a helper program. if it returns true/zero + # then we're inside a container. otherwise, no + subp([helper]) + return True + except (IOError, OSError): + pass + + # this code is largely from the logic in + # ubuntu's /etc/init/container-detect.conf + try: + # Detect old-style libvirt + # Detect OpenVZ containers + pid1env = get_proc_env(1) + if "container" in pid1env: + return True + if "LIBVIRT_LXC_UUID" in pid1env: + return True + except (IOError, OSError): + pass + + # Detect OpenVZ containers + if os.path.isdir("/proc/vz") and not os.path.isdir("/proc/bc"): + return True + + try: + # Detect Vserver containers + lines = load_file("/proc/self/status").splitlines() + for line in lines: + if line.startswith("VxID:"): + (_key, val) = line.strip().split(":", 1) + if val != "0": + return True + except (IOError, OSError): + pass return False +def get_proc_env(pid): + """ + Return the environment in a dict that a given process id was started with. + """ + + env = {} + fn = os.path.join("/proc/", str(pid), "environ") + try: + contents = load_file(fn) + toks = contents.split("\0") + for tok in toks: + if tok == "": + continue + (name, val) = tok.split("=", 1) + if name: + env[name] = val + except (IOError, OSError): + pass + return env + + def keyval_str_to_dict(kvstring): ret = {} for tok in kvstring.split(): @@ -851,5 +1396,4 @@ def keyval_str_to_dict(kvstring): key = tok val = True ret[key] = val - - return(ret) + return ret |