From 1e4cc0bc8f646f17af2b5ed1513ece49c4875acb Mon Sep 17 00:00:00 2001 From: Ryan Beisner Date: Thu, 24 Aug 2017 17:15:20 -0500 Subject: [PATCH] Sync charm-helpers Change-Id: Ifab2cdced938fc17ff0661742cd7c872cca97859 --- charmhelpers/contrib/charmsupport/nrpe.py | 2 +- .../contrib/hardening/apache/checks/config.py | 5 +- charmhelpers/contrib/openstack/utils.py | 4 +- charmhelpers/core/hookenv.py | 40 +++++++++++++ charmhelpers/core/host.py | 36 ++++++++++-- charmhelpers/fetch/ubuntu.py | 58 ++++++++++++------- tests/charmhelpers/core/hookenv.py | 40 +++++++++++++ 7 files changed, 152 insertions(+), 33 deletions(-) diff --git a/charmhelpers/contrib/charmsupport/nrpe.py b/charmhelpers/contrib/charmsupport/nrpe.py index 424b7f7..80d574d 100644 --- a/charmhelpers/contrib/charmsupport/nrpe.py +++ b/charmhelpers/contrib/charmsupport/nrpe.py @@ -125,7 +125,7 @@ class CheckException(Exception): class Check(object): - shortname_re = '[A-Za-z0-9-_]+$' + shortname_re = '[A-Za-z0-9-_.]+$' service_template = (""" #--------------------------------------------------- # This file is Juju managed diff --git a/charmhelpers/contrib/hardening/apache/checks/config.py b/charmhelpers/contrib/hardening/apache/checks/config.py index b18b263..06482aa 100644 --- a/charmhelpers/contrib/hardening/apache/checks/config.py +++ b/charmhelpers/contrib/hardening/apache/checks/config.py @@ -46,8 +46,9 @@ def get_audits(): context = ApacheConfContext() settings = utils.get_settings('apache') audits = [ - FilePermissionAudit(paths='/etc/apache2/apache2.conf', user='root', - group='root', mode=0o0640), + FilePermissionAudit(paths=os.path.join( + settings['common']['apache_dir'], 'apache2.conf'), + user='root', group='root', mode=0o0640), TemplatedFile(os.path.join(settings['common']['apache_dir'], 'mods-available/alias.conf'), diff --git a/charmhelpers/contrib/openstack/utils.py b/charmhelpers/contrib/openstack/utils.py index d15a631..837a167 100644 --- a/charmhelpers/contrib/openstack/utils.py +++ b/charmhelpers/contrib/openstack/utils.py @@ -186,7 +186,7 @@ SWIFT_CODENAMES = OrderedDict([ ('ocata', ['2.11.0', '2.12.0', '2.13.0']), ('pike', - ['2.13.0']), + ['2.13.0', '2.15.0']), ]) # >= Liberty version->codename mapping @@ -2051,7 +2051,7 @@ def snap_install_requested(): If openstack-origin is of the form snap:channel-series-release and channel is in SNAPS_CHANNELS return True. """ - origin = config('openstack-origin') + origin = config('openstack-origin') or "" if not origin.startswith('snap:'): return False diff --git a/charmhelpers/core/hookenv.py b/charmhelpers/core/hookenv.py index e44e22b..12f37b2 100644 --- a/charmhelpers/core/hookenv.py +++ b/charmhelpers/core/hookenv.py @@ -43,6 +43,7 @@ ERROR = "ERROR" WARNING = "WARNING" INFO = "INFO" DEBUG = "DEBUG" +TRACE = "TRACE" MARKER = object() cache = {} @@ -202,6 +203,27 @@ def service_name(): return local_unit().split('/')[0] +def principal_unit(): + """Returns the principal unit of this unit, otherwise None""" + # Juju 2.2 and above provides JUJU_PRINCIPAL_UNIT + principal_unit = os.environ.get('JUJU_PRINCIPAL_UNIT', None) + # If it's empty, then this unit is the principal + if principal_unit == '': + return os.environ['JUJU_UNIT_NAME'] + elif principal_unit is not None: + return principal_unit + # For Juju 2.1 and below, let's try work out the principle unit by + # the various charms' metadata.yaml. + for reltype in relation_types(): + for rid in relation_ids(reltype): + for unit in related_units(rid): + md = _metadata_unit(unit) + subordinate = md.pop('subordinate', None) + if not subordinate: + return unit + return None + + @cached def remote_service_name(relid=None): """The remote service name for a given relation-id (or the current relation)""" @@ -478,6 +500,21 @@ def metadata(): return yaml.safe_load(md) +def _metadata_unit(unit): + """Given the name of a unit (e.g. apache2/0), get the unit charm's + metadata.yaml. Very similar to metadata() but allows us to inspect + other units. Unit needs to be co-located, such as a subordinate or + principal/primary. + + :returns: metadata.yaml as a python object. + + """ + basedir = os.sep.join(charm_dir().split(os.sep)[:-2]) + unitdir = 'unit-{}'.format(unit.replace(os.sep, '-')) + with open(os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')) as md: + return yaml.safe_load(md) + + @cached def relation_types(): """Get a list of relation types supported by this charm""" @@ -753,6 +790,9 @@ class Hooks(object): def charm_dir(): """Return the root directory of the current charm""" + d = os.environ.get('JUJU_CHARM_DIR') + if d is not None: + return d return os.environ.get('CHARM_DIR') diff --git a/charmhelpers/core/host.py b/charmhelpers/core/host.py index b0043cb..5656e2f 100644 --- a/charmhelpers/core/host.py +++ b/charmhelpers/core/host.py @@ -34,7 +34,7 @@ import six from contextlib import contextmanager from collections import OrderedDict -from .hookenv import log +from .hookenv import log, DEBUG from .fstab import Fstab from charmhelpers.osplatform import get_platform @@ -487,13 +487,37 @@ def mkdir(path, owner='root', group='root', perms=0o555, force=False): def write_file(path, content, owner='root', group='root', perms=0o444): """Create or overwrite a file with the contents of a byte string.""" - log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) uid = pwd.getpwnam(owner).pw_uid gid = grp.getgrnam(group).gr_gid - with open(path, 'wb') as target: - os.fchown(target.fileno(), uid, gid) - os.fchmod(target.fileno(), perms) - target.write(content) + # lets see if we can grab the file and compare the context, to avoid doing + # a write. + existing_content = None + existing_uid, existing_gid = None, None + try: + with open(path, 'rb') as target: + existing_content = target.read() + stat = os.stat(path) + existing_uid, existing_gid = stat.st_uid, stat.st_gid + except: + pass + if content != existing_content: + log("Writing file {} {}:{} {:o}".format(path, owner, group, perms), + level=DEBUG) + with open(path, 'wb') as target: + os.fchown(target.fileno(), uid, gid) + os.fchmod(target.fileno(), perms) + target.write(content) + return + # the contents were the same, but we might still need to change the + # ownership. + if existing_uid != uid: + log("Changing uid on already existing content: {} -> {}" + .format(existing_uid, uid), level=DEBUG) + os.chown(path, uid, -1) + if existing_gid != gid: + log("Changing gid on already existing content: {} -> {}" + .format(existing_gid, gid), level=DEBUG) + os.chown(path, -1, gid) def fstab_remove(mp): diff --git a/charmhelpers/fetch/ubuntu.py b/charmhelpers/fetch/ubuntu.py index 545348f..40e1cb5 100644 --- a/charmhelpers/fetch/ubuntu.py +++ b/charmhelpers/fetch/ubuntu.py @@ -27,6 +27,7 @@ from charmhelpers.core.host import ( from charmhelpers.core.hookenv import ( log, DEBUG, + WARNING, ) from charmhelpers.fetch import SourceConfigError, GPGKeyError @@ -261,34 +262,47 @@ def apt_unhold(packages, fatal=False): return apt_mark(packages, 'unhold', fatal=fatal) -def import_key(keyid): - """Import a key in either ASCII Armor or Radix64 format. +def import_key(key): + """Import an ASCII Armor key. - `keyid` is either the keyid to fetch from a PGP server, or - the key in ASCII armor foramt. + /!\ A Radix64 format keyid is also supported for backwards + compatibility, but should never be used; the key retrieval + mechanism is insecure and subject to man-in-the-middle attacks + voiding all signature checks using that key. - :param keyid: String of key (or key id). + :param keyid: The key in ASCII armor format, + including BEGIN and END markers. :raises: GPGKeyError if the key could not be imported """ - key = keyid.strip() - if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and - key.endswith('-----END PGP PUBLIC KEY BLOCK-----')): + key = key.strip() + if '-' in key or '\n' in key: + # Send everything not obviously a keyid to GPG to import, as + # we trust its validation better than our own. eg. handling + # comments before the key. log("PGP key found (looks like ASCII Armor format)", level=DEBUG) - log("Importing ASCII Armor PGP key", level=DEBUG) - with NamedTemporaryFile() as keyfile: - with open(keyfile.name, 'w') as fd: - fd.write(key) - fd.write("\n") - cmd = ['apt-key', 'add', keyfile.name] - try: - subprocess.check_call(cmd) - except subprocess.CalledProcessError: - error = "Error importing PGP key '{}'".format(key) - log(error) - raise GPGKeyError(error) + if ('-----BEGIN PGP PUBLIC KEY BLOCK-----' in key and + '-----END PGP PUBLIC KEY BLOCK-----' in key): + log("Importing ASCII Armor PGP key", level=DEBUG) + with NamedTemporaryFile() as keyfile: + with open(keyfile.name, 'w') as fd: + fd.write(key) + fd.write("\n") + cmd = ['apt-key', 'add', keyfile.name] + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError: + error = "Error importing PGP key '{}'".format(key) + log(error) + raise GPGKeyError(error) + else: + raise GPGKeyError("ASCII armor markers missing from GPG key") else: - log("PGP key found (looks like Radix64 format)", level=DEBUG) - log("Importing PGP key from keyserver", level=DEBUG) + # We should only send things obviously not a keyid offsite + # via this unsecured protocol, as it may be a secret or part + # of one. + log("PGP key found (looks like Radix64 format)", level=WARNING) + log("INSECURLY importing PGP key from keyserver; " + "full key not provided.", level=WARNING) cmd = ['apt-key', 'adv', '--keyserver', 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key] try: diff --git a/tests/charmhelpers/core/hookenv.py b/tests/charmhelpers/core/hookenv.py index e44e22b..12f37b2 100644 --- a/tests/charmhelpers/core/hookenv.py +++ b/tests/charmhelpers/core/hookenv.py @@ -43,6 +43,7 @@ ERROR = "ERROR" WARNING = "WARNING" INFO = "INFO" DEBUG = "DEBUG" +TRACE = "TRACE" MARKER = object() cache = {} @@ -202,6 +203,27 @@ def service_name(): return local_unit().split('/')[0] +def principal_unit(): + """Returns the principal unit of this unit, otherwise None""" + # Juju 2.2 and above provides JUJU_PRINCIPAL_UNIT + principal_unit = os.environ.get('JUJU_PRINCIPAL_UNIT', None) + # If it's empty, then this unit is the principal + if principal_unit == '': + return os.environ['JUJU_UNIT_NAME'] + elif principal_unit is not None: + return principal_unit + # For Juju 2.1 and below, let's try work out the principle unit by + # the various charms' metadata.yaml. + for reltype in relation_types(): + for rid in relation_ids(reltype): + for unit in related_units(rid): + md = _metadata_unit(unit) + subordinate = md.pop('subordinate', None) + if not subordinate: + return unit + return None + + @cached def remote_service_name(relid=None): """The remote service name for a given relation-id (or the current relation)""" @@ -478,6 +500,21 @@ def metadata(): return yaml.safe_load(md) +def _metadata_unit(unit): + """Given the name of a unit (e.g. apache2/0), get the unit charm's + metadata.yaml. Very similar to metadata() but allows us to inspect + other units. Unit needs to be co-located, such as a subordinate or + principal/primary. + + :returns: metadata.yaml as a python object. + + """ + basedir = os.sep.join(charm_dir().split(os.sep)[:-2]) + unitdir = 'unit-{}'.format(unit.replace(os.sep, '-')) + with open(os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')) as md: + return yaml.safe_load(md) + + @cached def relation_types(): """Get a list of relation types supported by this charm""" @@ -753,6 +790,9 @@ class Hooks(object): def charm_dir(): """Return the root directory of the current charm""" + d = os.environ.get('JUJU_CHARM_DIR') + if d is not None: + return d return os.environ.get('CHARM_DIR')