diff options
author | Joshua Harlow <harlowja@yahoo-inc.com> | 2012-06-11 17:21:43 -0700 |
---|---|---|
committer | Joshua Harlow <harlowja@yahoo-inc.com> | 2012-06-11 17:21:43 -0700 |
commit | eeff4524aceaa35577ab74a1066b4c169f9e865d (patch) | |
tree | b1cca489d760fd8a8321b55d00483daa5692da50 /cloudinit/util.py | |
parent | feeb0620b31d2d6aff59449c53a17b193a2d8d8e (diff) | |
download | vyos-cloud-init-eeff4524aceaa35577ab74a1066b4c169f9e865d.tar.gz vyos-cloud-init-eeff4524aceaa35577ab74a1066b4c169f9e865d.zip |
Pylint cleanups, add a load yaml function that handles errors nicely + other cleanups.
Diffstat (limited to 'cloudinit/util.py')
-rw-r--r-- | cloudinit/util.py | 295 |
1 files changed, 232 insertions, 63 deletions
diff --git a/cloudinit/util.py b/cloudinit/util.py index 5930ff3f..6cf75916 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -23,6 +23,8 @@ from StringIO import StringIO import contextlib +import copy +import errno import glob import grp import gzip @@ -32,6 +34,8 @@ import pwd import shutil import socket import subprocess +import sys +import tempfile import types import urlparse @@ -40,6 +44,8 @@ import yaml from cloudinit import log as logging from cloudinit import url_helper as uhelp +from cloudinit.settings import (CFG_BUILTIN, CLOUD_CONFIG) + try: import selinux @@ -55,6 +61,9 @@ FN_REPLACEMENTS = { os.sep: '_', } +# Helper utils to see if running in a container +CONTAINER_TESTS = ['running-in-container', 'lxc-is-container'] + class ProcessExecutionError(IOError): @@ -112,12 +121,17 @@ class SeLinuxGuard(object): def __enter__(self): return self.engaged - def __exit__(self, type, value, traceback): + def __exit__(self, excp_type, excp_value, excp_traceback): if self.engaged: - LOG.debug("Disengaging selinux mode for %s: %s", self.path, self.recursive) + LOG.debug("Disengaging selinux mode for %s: %s", + self.path, self.recursive) selinux.restorecon(self.path, recursive=self.recursive) +class MountFailedError(Exception): + pass + + def translate_bool(val): if not val: return False @@ -130,14 +144,12 @@ def translate_bool(val): def read_conf(fname): try: - mp = yaml.load(load_file(fname)) - if not isinstance(mp, (dict)): - return {} - return mp + return load_yaml(load_file(fname), default={}) except IOError as e: if e.errno == errno.ENOENT: return {} - raise + else: + raise def clean_filename(fn): @@ -148,8 +160,9 @@ def clean_filename(fn): def decomp_str(data): try: - uncomp = gzip.GzipFile(None, "rb", 1, StringIO(data)).read() - return uncomp + buf = StringIO(str(data)) + with contextlib.closing(gzip.GzipFile(None, "rb", 1, buf)) as gh: + return gh.read() except: return data @@ -180,16 +193,13 @@ def is_ipv4(instr): return (len(toks) == 4) -def get_base_cfg(cfgfile, cfg_builtin=None): +def merge_base_cfg(cfgfile, cfg_builtin=None): syscfg = read_conf_with_confd(cfgfile) kern_contents = read_cc_from_cmdline() kerncfg = {} if kern_contents: - try: - kerncfg = yaml.load(kern_contents) - except: - pass + kerncfg = load_yaml(kern_contents, default={}) # kernel parameters override system config combined = mergedict(kerncfg, syscfg) @@ -265,8 +275,9 @@ def obj_name(obj): 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(): @@ -276,9 +287,11 @@ def mergedict(src, cand): src[k] = mergedict(src[k], v) else: if not isinstance(src, dict): - raise TypeError("Attempting to merge a non dictionary source type: %s" % (type(src))) + raise TypeError(("Attempting to merge a non dictionary " + "source type: %s") % (obj_name(src))) if not isinstance(cand, dict): - raise TypeError("Attempting to merge a non dictionary candiate type: %s" % (type(cand))) + raise TypeError(("Attempting to merge a non dictionary " + "candidate type: %s") % (obj_name(cand))) return src @@ -308,8 +321,9 @@ def del_dir(path): shutil.rmtree(path) -# get keyid from keyserver +# get gpg keyid from keyserver def getkeybyid(keyid, keyserver): + # TODO fix this... shcmd = """ k=${1} ks=${2}; exec 2>/dev/null @@ -323,7 +337,7 @@ def getkeybyid(keyid, keyserver): [ -n "${armour}" ] && echo "${armour}" """ args = ['sh', '-c', shcmd, "export-gpg-keyid", keyid, keyserver] - (stdout, stderr) = subp(args) + (stdout, _stderr) = subp(args) return stdout @@ -340,11 +354,12 @@ def runparts(dirp, skip_no_exist=True): try: subp([exe_path]) except ProcessExecutionError as e: - LOG.exception("Failed running %s [%i]", exe_path, e.exit_code) + LOG.exception("Failed running %s [%s]", exe_path, e.exit_code) failed += 1 if failed and attempted: - raise RuntimeError('runparts: %i failures in %i attempted commands' % (failed, attempted)) + raise RuntimeError('Runparts: %s failures in %s attempted commands' + % (failed, attempted)) # read_optional_seed @@ -363,6 +378,32 @@ def read_optional_seed(fill, base="", ext="", timeout=5): raise +def read_file_or_url(url, timeout, retries, file_retries): + 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.load(blob) + if not isinstance(converted, allowed): + # Yes this will just be caught, but thats ok for now... + raise TypeError("Yaml load allows %s types, but got %s instead" % + (allowed, obj_name(converted))) + loaded = converted + except (yaml.YAMLError, TypeError, ValueError) as exc: + LOG.exception("Failed loading yaml due to: %s", exc) + return loaded + + def read_seeded(base="", ext="", timeout=5, retries=10, file_retries=0): if base.startswith("/"): base = "file://%s" % base @@ -378,13 +419,16 @@ 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) - (md_str, msc) = uhelp.readurl(md_url, timeout=timeout, retries=retries) - (ud, usc) = uhelp.readurl(ud_url, timeout=timeout, retries=retries) + (md_str, msc) = read_file_or_url(md_url, timeout, retries, file_retries) md = None if md_str and uhelp.ok_http_code(msc): - md = yaml.load(md_str) - if not uhelp.ok_http_code(usc): - ud = None + md = load_yaml(md_str, default={}) + + (ud_str, usc) = read_file_or_url(ud_url, timeout, retries, file_retries) + ud = None + if ud_str and uhelp.ok_http_code(usc): + ud = ud_str + return (md, ud) @@ -410,13 +454,14 @@ def read_conf_with_confd(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 RuntimeError(("Config file %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 = confd.strip() + confd = str(confd).strip() elif os.path.isdir("%s.d" % cfgfile): confd = "%s.d" % cfgfile @@ -490,26 +535,41 @@ def get_hostname_fqdn(cfg, cloud): 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: for line in load_file(filename).splitlines(): hashpos = line.find("#") if hashpos >= 0: line = line[0:hashpos] - toks = line.split() - - # if there there is less than 3 entries (ip, canonical, alias) + 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 as e: + except IOError: pass return fqdn @@ -584,7 +644,7 @@ def close_stdin(): os.dup2(fp.fileno(), sys.stdin.fileno()) -def find_devs_with(criteria): +def find_devs_with(criteria=None): """ find devices matching given criteria (via blkid) criteria can be *one* of: @@ -593,10 +653,26 @@ def find_devs_with(criteria): UUID=<uuid> """ try: - (out, _err) = subp(['blkid', '-t%s' % criteria, '-odevice']) + blk_id_cmd = ['blkid'] + 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. + blk_id_cmd.append("-t%s" % (criteria)) + # Only print the device name + blk_id_cmd.append('-odevice') + (out, _err) = subp(blk_id_cmd) + entries = [] + for line in out.splitlines(): + line = line.strip() + if line: + entries.append(line) + return entries except ProcessExecutionError: return [] - return (out.splitlines()) def load_file(fname, read_cb=None): @@ -604,7 +680,10 @@ def load_file(fname, read_cb=None): with open(fname, 'rb') as fh: ofh = StringIO() pipe_in_out(fh, ofh, chunk_cb=read_cb) - return ofh.getvalue() + ofh.flush() + contents = ofh.getvalue() + LOG.debug("Read %s bytes from %s", len(contents), fname) + return contents def get_cmdline(): @@ -620,7 +699,8 @@ def get_cmdline(): def pipe_in_out(in_fh, out_fh, chunk_size=1024, chunk_cb=None): bytes_piped = 0 - LOG.debug("Transferring the contents of %s to %s in chunks of size %s.", in_fh, out_fh, chunk_size) + LOG.debug(("Transferring the contents of %s " + "to %s in chunks of size %sb"), in_fh, out_fh, chunk_size) while True: data = in_fh.read(chunk_size) if data == '': @@ -658,15 +738,87 @@ def ensure_dirs(dirlist, mode=0755): def ensure_dir(path, mode=0755): if not os.path.isdir(path): - fixmodes = [] - LOG.debug("Ensuring directory exists at path %s (perms=%s)", dir_name, mode) - try: - os.makedirs(path) - except OSError as e: - if e.errno != errno.EEXIST: - raise e - if mode is not None: - os.chmod(path, mode) + # Make the dir and adjust the mode + LOG.debug("Ensuring directory exists at path %s", path) + os.makedirs(path) + chmod(path, mode) + else: + # Just adjust the mode + chmod(path, mode) + + +def get_base_cfg(cfg_path=None): + if not cfg_path: + cfg_path = CLOUD_CONFIG + return merge_base_cfg(cfg_path, get_builtin_cfg()) + + +@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 if it was already mounted + mount_locs = load_file("/proc/mounts").splitlines() + for mpline in mount_locs: + # Format at: http://linux.die.net/man/5/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] = (dev, fstype, mp, False) + except (IOError, OSError): + pass + return mounted + + +def mount_cb(device, callback, data=None, rw=False): + """ + 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. + """ + mounted = mounts() + with tempdir() as tmpd: + umount = False + if device in mounted: + mountpoint = "%s/" % mounted[device][2] + else: + try: + mountcmd = ['mount', "-o"] + if rw: + mountcmd.append('rw') + else: + mountcmd.append('ro') + mountcmd.append(device) + mountcmd.append(tmpd) + subp(mountcmd) + umount = tmpd + except IOError as exc: + raise MountFailedError("%s" % (exc)) + mountpoint = "%s/" % tmpd + with unmounter(umount): + if data is None: + ret = callback(mountpoint) + else: + ret = callback(mountpoint, data) + return ret + + +def get_builtin_cfg(): + # Deep copy so that others can't modify + return copy.deepcopy(CFG_BUILTIN) def sym_link(source, link): @@ -687,6 +839,18 @@ def ensure_file(path): write_file(path, content='', omode="ab") +def chmod(path, mode): + real_mode = None + try: + real_mode = int(mode) + except (ValueError, TypeError): + pass + if path and real_mode: + LOG.debug("Adjusting the permissions of %s (perms=%o)", + path, real_mode) + os.chmod(path, real_mode) + + def write_file(filename, content, mode=0644, omode="wb"): """ Writes a file with the given content and sets the file mode as specified. @@ -698,13 +862,12 @@ def write_file(filename, content, mode=0644, omode="wb"): @param omode: The open mode used when opening the file (r, rb, a, etc.) """ ensure_dir(os.path.dirname(filename)) - LOG.debug("Writing to %s - %s (perms=%s) %s bytes", filename, omode, mode, len(content)) + LOG.debug("Writing to %s - %s, %s bytes", filename, omode, len(content)) with open(filename, omode) as fh: with SeLinuxGuard(filename): fh.write(content) fh.flush() - if mode is not None: - os.chmod(filename, mode) + chmod(filename, mode) def delete_dir_contents(dirname): @@ -725,7 +888,8 @@ def subp(args, input_data=None, allowed_rc=None, env=None): if allowed_rc is None: allowed_rc = [0] try: - LOG.debug("Running command %s with allowed return codes %s", args, allowed_rc) + LOG.debug("Running command %s with allowed return codes %s", + args, allowed_rc) sp = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=env) @@ -768,14 +932,16 @@ def shellify(cmdlist, add_header=True): def is_container(): - # is this code running in a container of some sort + """ + Checks to see if this code running in a container of some sort + """ - for helper in ('running-in-container', 'lxc-is-container'): + 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 cmd = [helper] - (stdout, stderr) = subp(cmd, allowed_rc=[0]) + subp(cmd, allowed_rc=[0]) return True except (IOError, OSError): pass @@ -812,7 +978,10 @@ def is_container(): def get_proc_env(pid): - # return the environment in a dict that a given process id was started with + """ + Return the environment in a dict that a given process id was started with. + """ + env = {} fn = os.path.join("/proc/", str(pid), "environ") try: |