Charmhelpers sync to get vaultlocker fixes
Also gate checking vault context completing on whether dependencies are installed. Change-Id: Ib424abe608081da21207db262fb82362f23fe6ca Closes-Bug: #1849323
This commit is contained in:
parent
9a67dc3d2d
commit
60dd2f0189
@ -35,8 +35,9 @@ Examples:
|
||||
>>> ufw.enable()
|
||||
>>> ufw.service('4949', 'close') # munin
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import subprocess
|
||||
|
||||
from charmhelpers.core import hookenv
|
||||
@ -236,29 +237,45 @@ def default_policy(policy='deny', direction='incoming'):
|
||||
|
||||
|
||||
def modify_access(src, dst='any', port=None, proto=None, action='allow',
|
||||
index=None):
|
||||
index=None, prepend=False, comment=None):
|
||||
"""
|
||||
Grant access to an address or subnet
|
||||
|
||||
:param src: address (e.g. 192.168.1.234) or subnet
|
||||
(e.g. 192.168.1.0/24).
|
||||
:type src: Optional[str]
|
||||
:param dst: destiny of the connection, if the machine has multiple IPs and
|
||||
connections to only one of those have to accepted this is the
|
||||
field has to be set.
|
||||
:type dst: Optional[str]
|
||||
:param port: destiny port
|
||||
:type port: Optional[int]
|
||||
:param proto: protocol (tcp or udp)
|
||||
:type proto: Optional[str]
|
||||
:param action: `allow` or `delete`
|
||||
:type action: str
|
||||
:param index: if different from None the rule is inserted at the given
|
||||
`index`.
|
||||
:type index: Optional[int]
|
||||
:param prepend: Whether to insert the rule before all other rules matching
|
||||
the rule's IP type.
|
||||
:type prepend: bool
|
||||
:param comment: Create the rule with a comment
|
||||
:type comment: Optional[str]
|
||||
"""
|
||||
if not is_enabled():
|
||||
hookenv.log('ufw is disabled, skipping modify_access()', level='WARN')
|
||||
return
|
||||
|
||||
if action == 'delete':
|
||||
cmd = ['ufw', 'delete', 'allow']
|
||||
if index is not None:
|
||||
cmd = ['ufw', '--force', 'delete', str(index)]
|
||||
else:
|
||||
cmd = ['ufw', 'delete', 'allow']
|
||||
elif index is not None:
|
||||
cmd = ['ufw', 'insert', str(index), action]
|
||||
elif prepend:
|
||||
cmd = ['ufw', 'prepend', action]
|
||||
else:
|
||||
cmd = ['ufw', action]
|
||||
|
||||
@ -274,6 +291,9 @@ def modify_access(src, dst='any', port=None, proto=None, action='allow',
|
||||
if proto is not None:
|
||||
cmd += ['proto', proto]
|
||||
|
||||
if comment:
|
||||
cmd.extend(['comment', comment])
|
||||
|
||||
hookenv.log('ufw {}: {}'.format(action, ' '.join(cmd)), level='DEBUG')
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
@ -337,3 +357,33 @@ def service(name, action):
|
||||
else:
|
||||
raise UFWError(("'{}' not supported, use 'allow' "
|
||||
"or 'delete'").format(action))
|
||||
|
||||
|
||||
def status():
|
||||
"""Retrieve firewall rules as represented by UFW.
|
||||
|
||||
:returns: Tuples with rule number and data
|
||||
(1, {'to': '', 'action':, 'from':, '', ipv6: True, 'comment': ''})
|
||||
:rtype: Iterator[Tuple[int, Dict[str, Union[bool, str]]]]
|
||||
"""
|
||||
if six.PY2:
|
||||
raise RuntimeError('Call to function not supported on Python2')
|
||||
cp = subprocess.run(('ufw', 'status', 'numbered',),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
check=True, universal_newlines=True)
|
||||
for line in cp.stdout.splitlines():
|
||||
if not line.startswith('['):
|
||||
continue
|
||||
ipv6 = True if '(v6)' in line else False
|
||||
line = line.replace('(v6)', '')
|
||||
line = line.replace('[', '')
|
||||
line = line.replace(']', '')
|
||||
line = line.replace('Anywhere', 'any')
|
||||
row = line.split()
|
||||
yield (int(row[0]), {
|
||||
'to': row[1],
|
||||
'action': ' '.join(row[2:4]).lower(),
|
||||
'from': row[4],
|
||||
'ipv6': ipv6,
|
||||
'comment': row[6] if len(row) > 5 and row[5] == '#' else '',
|
||||
})
|
||||
|
@ -157,10 +157,11 @@ def generate_ha_relation_data(service,
|
||||
_relation_data = {'resources': {}, 'resource_params': {}}
|
||||
|
||||
if haproxy_enabled:
|
||||
_meta = 'meta migration-threshold="INFINITY" failure-timeout="5s"'
|
||||
_haproxy_res = 'res_{}_haproxy'.format(service)
|
||||
_relation_data['resources'] = {_haproxy_res: 'lsb:haproxy'}
|
||||
_relation_data['resource_params'] = {
|
||||
_haproxy_res: 'op monitor interval="5s"'
|
||||
_haproxy_res: '{} op monitor interval="5s"'.format(_meta)
|
||||
}
|
||||
_relation_data['init_services'] = {_haproxy_res: 'haproxy'}
|
||||
_relation_data['clones'] = {
|
||||
|
@ -21,6 +21,7 @@ import sys
|
||||
import yaml
|
||||
import zipfile
|
||||
|
||||
import charmhelpers
|
||||
import charmhelpers.core.hookenv as hookenv
|
||||
import charmhelpers.core.host as ch_host
|
||||
|
||||
@ -234,7 +235,10 @@ def maybe_do_policyd_overrides(openstack_release,
|
||||
blacklist_paths=None,
|
||||
blacklist_keys=None,
|
||||
template_function=None,
|
||||
restart_handler=None):
|
||||
restart_handler=None,
|
||||
user=None,
|
||||
group=None,
|
||||
config_changed=False):
|
||||
"""If the config option is set, get the resource file and process it to
|
||||
enable the policy.d overrides for the service passed.
|
||||
|
||||
@ -263,6 +267,11 @@ def maybe_do_policyd_overrides(openstack_release,
|
||||
directory. However, for any services where this is buggy then a
|
||||
restart_handler can be used to force the policy.d files to be read.
|
||||
|
||||
If the config_changed param is True, then the handling is slightly
|
||||
different: It will only perform the policyd overrides if the config is True
|
||||
and the success file doesn't exist. Otherwise, it does nothing as the
|
||||
resource file has already been processed.
|
||||
|
||||
:param openstack_release: The openstack release that is installed.
|
||||
:type openstack_release: str
|
||||
:param service: the service name to construct the policy.d directory for.
|
||||
@ -278,21 +287,29 @@ def maybe_do_policyd_overrides(openstack_release,
|
||||
:param restart_handler: The function to call if the service should be
|
||||
restarted.
|
||||
:type restart_handler: Union[None, Callable[]]
|
||||
:param user: The user to create/write files/directories as
|
||||
:type user: Union[None, str]
|
||||
:param group: the group to create/write files/directories as
|
||||
:type group: Union[None, str]
|
||||
:param config_changed: Set to True for config_changed hook.
|
||||
:type config_changed: bool
|
||||
"""
|
||||
_user = service if user is None else user
|
||||
_group = service if group is None else group
|
||||
if not is_policyd_override_valid_on_this_release(openstack_release):
|
||||
return
|
||||
hookenv.log("Running maybe_do_policyd_overrides",
|
||||
level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
if not is_policyd_override_valid_on_this_release(openstack_release):
|
||||
hookenv.log("... policy overrides not valid on this release: {}"
|
||||
.format(openstack_release),
|
||||
level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
return
|
||||
config = hookenv.config()
|
||||
try:
|
||||
if not config.get(POLICYD_CONFIG_NAME, False):
|
||||
clean_policyd_dir_for(service, blacklist_paths)
|
||||
if (os.path.isfile(_policy_success_file()) and
|
||||
restart_handler is not None and
|
||||
callable(restart_handler)):
|
||||
clean_policyd_dir_for(service,
|
||||
blacklist_paths,
|
||||
user=_user,
|
||||
group=_group)
|
||||
if (os.path.isfile(_policy_success_file())
|
||||
and restart_handler is not None
|
||||
and callable(restart_handler)):
|
||||
restart_handler()
|
||||
remove_policy_success_file()
|
||||
return
|
||||
@ -302,6 +319,12 @@ def maybe_do_policyd_overrides(openstack_release,
|
||||
import traceback
|
||||
hookenv.log(traceback.format_exc(), level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
return
|
||||
# if the policyd overrides have been performed when doing config_changed
|
||||
# just return
|
||||
if config_changed and is_policy_success_file_set():
|
||||
hookenv.log("... already setup, so skipping.",
|
||||
level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
return
|
||||
# from now on it should succeed; if it doesn't then status line will show
|
||||
# broken.
|
||||
resource_filename = get_policy_resource_filename()
|
||||
@ -312,63 +335,18 @@ def maybe_do_policyd_overrides(openstack_release,
|
||||
restart_handler()
|
||||
|
||||
|
||||
def maybe_do_policyd_overrides_on_config_changed(openstack_release,
|
||||
service,
|
||||
blacklist_paths=None,
|
||||
blacklist_keys=None,
|
||||
template_function=None,
|
||||
restart_handler=None):
|
||||
"""This function is designed to be called from the config changed hook
|
||||
handler. It will only perform the policyd overrides if the config is True
|
||||
and the success file doesn't exist. Otherwise, it does nothing as the
|
||||
resource file has already been processed.
|
||||
@charmhelpers.deprecate("Use maybe_do_poliyd_overrrides instead")
|
||||
def maybe_do_policyd_overrides_on_config_changed(*args, **kwargs):
|
||||
"""This function is designed to be called from the config changed hook.
|
||||
|
||||
DEPRECATED: please use maybe_do_policyd_overrides() with the param
|
||||
`config_changed` as `True`.
|
||||
|
||||
See maybe_do_policyd_overrides() for more details on the params.
|
||||
|
||||
:param openstack_release: The openstack release that is installed.
|
||||
:type openstack_release: str
|
||||
:param service: the service name to construct the policy.d directory for.
|
||||
:type service: str
|
||||
:param blacklist_paths: optional list of paths to leave alone
|
||||
:type blacklist_paths: Union[None, List[str]]
|
||||
:param blacklist_keys: optional list of keys that mustn't appear in the
|
||||
yaml file's
|
||||
:type blacklist_keys: Union[None, List[str]]
|
||||
:param template_function: Optional function that can modify the string
|
||||
prior to being processed as a Yaml document.
|
||||
:type template_function: Union[None, Callable[[str], str]]
|
||||
:param restart_handler: The function to call if the service should be
|
||||
restarted.
|
||||
:type restart_handler: Union[None, Callable[]]
|
||||
"""
|
||||
if not is_policyd_override_valid_on_this_release(openstack_release):
|
||||
return
|
||||
hookenv.log("Running maybe_do_policyd_overrides_on_config_changed",
|
||||
level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
config = hookenv.config()
|
||||
try:
|
||||
if not config.get(POLICYD_CONFIG_NAME, False):
|
||||
clean_policyd_dir_for(service, blacklist_paths)
|
||||
if (os.path.isfile(_policy_success_file()) and
|
||||
restart_handler is not None and
|
||||
callable(restart_handler)):
|
||||
restart_handler()
|
||||
remove_policy_success_file()
|
||||
return
|
||||
except Exception as e:
|
||||
hookenv.log("... ERROR: Exception is: {}".format(str(e)),
|
||||
level=POLICYD_CONFIG_NAME)
|
||||
import traceback
|
||||
hookenv.log(traceback.format_exc(), level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
return
|
||||
# if the policyd overrides have been performed just return
|
||||
if os.path.isfile(_policy_success_file()):
|
||||
hookenv.log("... already setup, so skipping.",
|
||||
level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
return
|
||||
maybe_do_policyd_overrides(
|
||||
openstack_release, service, blacklist_paths, blacklist_keys,
|
||||
template_function, restart_handler)
|
||||
if 'config_changed' not in kwargs.keys():
|
||||
kwargs['config_changed'] = True
|
||||
return maybe_do_policyd_overrides(*args, **kwargs)
|
||||
|
||||
|
||||
def get_policy_resource_filename():
|
||||
@ -385,13 +363,16 @@ def get_policy_resource_filename():
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open_and_filter_yaml_files(filepath):
|
||||
def open_and_filter_yaml_files(filepath, has_subdirs=False):
|
||||
"""Validate that the filepath provided is a zip file and contains at least
|
||||
one (.yaml|.yml) file, and that the files are not duplicated when the zip
|
||||
file is flattened. Note that the yaml files are not checked. This is the
|
||||
first stage in validating the policy zipfile; individual yaml files are not
|
||||
checked for validity or black listed keys.
|
||||
|
||||
If the has_subdirs param is True, then the files are flattened to the first
|
||||
directory, and the files in the root are ignored.
|
||||
|
||||
An example of use is:
|
||||
|
||||
with open_and_filter_yaml_files(some_path) as zfp, g:
|
||||
@ -400,6 +381,8 @@ def open_and_filter_yaml_files(filepath):
|
||||
|
||||
:param filepath: a filepath object that can be opened by zipfile
|
||||
:type filepath: Union[AnyStr, os.PathLike[AntStr]]
|
||||
:param has_subdirs: Keep first level of subdirectories in yaml file.
|
||||
:type has_subdirs: bool
|
||||
:returns: (zfp handle,
|
||||
a generator of the (name, filename, ZipInfo object) tuples) as a
|
||||
tuple.
|
||||
@ -412,7 +395,7 @@ def open_and_filter_yaml_files(filepath):
|
||||
with zipfile.ZipFile(filepath, 'r') as zfp:
|
||||
# first pass through; check for duplicates and at least one yaml file.
|
||||
names = collections.defaultdict(int)
|
||||
yamlfiles = _yamlfiles(zfp)
|
||||
yamlfiles = _yamlfiles(zfp, has_subdirs)
|
||||
for name, _, _, _ in yamlfiles:
|
||||
names[name] += 1
|
||||
# There must be at least 1 yaml file.
|
||||
@ -428,17 +411,33 @@ def open_and_filter_yaml_files(filepath):
|
||||
yield (zfp, yamlfiles)
|
||||
|
||||
|
||||
def _yamlfiles(zipfile):
|
||||
def _yamlfiles(zipfile, has_subdirs=False):
|
||||
"""Helper to get a yaml file (according to POLICYD_VALID_EXTS extensions)
|
||||
and the infolist item from a zipfile.
|
||||
|
||||
If the `has_subdirs` param is True, the the only yaml files that have a
|
||||
directory component are read, and then first part of the directory
|
||||
component is kept, along with the filename in the name. e.g. an entry with
|
||||
a filename of:
|
||||
|
||||
compute/someotherdir/override.yaml
|
||||
|
||||
is returned as:
|
||||
|
||||
compute/override, yaml, override.yaml, <ZipInfo object>
|
||||
|
||||
This is to help with the special, additional, processing that the dashboard
|
||||
charm requires.
|
||||
|
||||
:param zipfile: the zipfile to read zipinfo items from
|
||||
:type zipfile: zipfile.ZipFile
|
||||
:returns: generator of (name, ext, filename, info item) for each self-identified
|
||||
yaml file.
|
||||
:param has_subdirs: Keep first level of subdirectories in yaml file.
|
||||
:type has_subdirs: bool
|
||||
:returns: generator of (name, ext, filename, info item) for each
|
||||
self-identified yaml file.
|
||||
:rtype: List[(str, str, str, zipfile.ZipInfo)]
|
||||
"""
|
||||
l = []
|
||||
files = []
|
||||
for infolist_item in zipfile.infolist():
|
||||
try:
|
||||
if infolist_item.is_dir():
|
||||
@ -447,12 +446,14 @@ def _yamlfiles(zipfile):
|
||||
# fallback to "old" way to determine dir entry for pre-py36
|
||||
if infolist_item.filename.endswith('/'):
|
||||
continue
|
||||
_, name_ext = os.path.split(infolist_item.filename)
|
||||
_dir, name_ext = os.path.split(infolist_item.filename)
|
||||
name, ext = os.path.splitext(name_ext)
|
||||
if has_subdirs and _dir != "":
|
||||
name = os.path.join(_dir.split(os.path.sep)[0], name)
|
||||
ext = ext.lower()
|
||||
if ext and ext in POLICYD_VALID_EXTS:
|
||||
l.append((name, ext, name_ext, infolist_item))
|
||||
return l
|
||||
files.append((name, ext, name_ext, infolist_item))
|
||||
return files
|
||||
|
||||
|
||||
def read_and_validate_yaml(stream_or_doc, blacklist_keys=None):
|
||||
@ -498,9 +499,6 @@ def read_and_validate_yaml(stream_or_doc, blacklist_keys=None):
|
||||
def policyd_dir_for(service):
|
||||
"""Return the policy directory for the named service.
|
||||
|
||||
This assumes the default name of "policy.d" which is kept across all
|
||||
charms.
|
||||
|
||||
:param service: str
|
||||
:returns: the policy.d override directory.
|
||||
:rtype: os.PathLike[str]
|
||||
@ -508,7 +506,7 @@ def policyd_dir_for(service):
|
||||
return os.path.join("/", "etc", service, "policy.d")
|
||||
|
||||
|
||||
def clean_policyd_dir_for(service, keep_paths=None):
|
||||
def clean_policyd_dir_for(service, keep_paths=None, user=None, group=None):
|
||||
"""Clean out the policyd directory except for items that should be kept.
|
||||
|
||||
The keep_paths, if used, should be set to the full path of the files that
|
||||
@ -521,11 +519,18 @@ def clean_policyd_dir_for(service, keep_paths=None):
|
||||
:type service: str
|
||||
:param keep_paths: optional list of paths to not delete.
|
||||
:type keep_paths: Union[None, List[str]]
|
||||
:param user: The user to create/write files/directories as
|
||||
:type user: Union[None, str]
|
||||
:param group: the group to create/write files/directories as
|
||||
:type group: Union[None, str]
|
||||
"""
|
||||
_user = service if user is None else user
|
||||
_group = service if group is None else group
|
||||
keep_paths = keep_paths or []
|
||||
path = policyd_dir_for(service)
|
||||
hookenv.log("Cleaning path: {}".format(path), level=hookenv.DEBUG)
|
||||
if not os.path.exists(path):
|
||||
ch_host.mkdir(path, owner=service, group=service, perms=0o775)
|
||||
ch_host.mkdir(path, owner=_user, group=_group, perms=0o775)
|
||||
_scanner = os.scandir if sys.version_info > (3, 4) else _py2_scandir
|
||||
for direntry in _scanner(path):
|
||||
# see if the path should be kept.
|
||||
@ -538,6 +543,22 @@ def clean_policyd_dir_for(service, keep_paths=None):
|
||||
os.remove(direntry.path)
|
||||
|
||||
|
||||
def maybe_create_directory_for(path, user, group):
|
||||
"""For the filename 'path', ensure that the directory for that path exists.
|
||||
|
||||
Note that if the directory already exists then the permissions are NOT
|
||||
changed.
|
||||
|
||||
:param path: the filename including the path to it.
|
||||
:type path: str
|
||||
:param user: the user to create the directory as
|
||||
:param group: the group to create the directory as
|
||||
"""
|
||||
_dir, _ = os.path.split(path)
|
||||
if not os.path.exists(_dir):
|
||||
ch_host.mkdir(_dir, owner=user, group=group, perms=0o775)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _py2_scandir(path):
|
||||
"""provide a py2 implementation of os.scandir if this module ever gets used
|
||||
@ -573,6 +594,11 @@ def path_for_policy_file(service, name):
|
||||
It is constructed using policyd_dir_for(), the name and the ".yaml"
|
||||
extension.
|
||||
|
||||
For horizon, for example, it's a bit more complicated. The name param is
|
||||
actually "override_service_dir/a_name", where target_service needs to be
|
||||
one the allowed horizon override services. This translation and check is
|
||||
done in the _yamlfiles() function.
|
||||
|
||||
:param service: the service name
|
||||
:type service: str
|
||||
:param name: the name for the policy override
|
||||
@ -600,6 +626,22 @@ def remove_policy_success_file():
|
||||
pass
|
||||
|
||||
|
||||
def set_policy_success_file():
|
||||
"""Set the file that indicates successful policyd override."""
|
||||
open(_policy_success_file(), "w").close()
|
||||
|
||||
|
||||
def is_policy_success_file_set():
|
||||
"""Returns True if the policy success file has been set.
|
||||
|
||||
This indicates that policies are overridden and working properly.
|
||||
|
||||
:returns: True if the policy file is set
|
||||
:rtype: bool
|
||||
"""
|
||||
return os.path.isfile(_policy_success_file())
|
||||
|
||||
|
||||
def policyd_status_message_prefix():
|
||||
"""Return the prefix str for the status line.
|
||||
|
||||
@ -609,7 +651,7 @@ def policyd_status_message_prefix():
|
||||
:returns: the prefix
|
||||
:rtype: str
|
||||
"""
|
||||
if os.path.isfile(_policy_success_file()):
|
||||
if is_policy_success_file_set():
|
||||
return "PO:"
|
||||
return "PO (broken):"
|
||||
|
||||
@ -618,7 +660,11 @@ def process_policy_resource_file(resource_file,
|
||||
service,
|
||||
blacklist_paths=None,
|
||||
blacklist_keys=None,
|
||||
template_function=None):
|
||||
template_function=None,
|
||||
preserve_topdir=False,
|
||||
preprocess_filename=None,
|
||||
user=None,
|
||||
group=None):
|
||||
"""Process the resource file (which should contain at least one yaml file)
|
||||
and write those files to the service's policy.d directory.
|
||||
|
||||
@ -638,6 +684,16 @@ def process_policy_resource_file(resource_file,
|
||||
its file path reconstructed. This, also, must not match any path in the
|
||||
black list.
|
||||
|
||||
The yaml filename can be modified in two ways. If the `preserve_topdir`
|
||||
param is True, then files will be flattened to the top dir. This allows
|
||||
for creating sets of files that can be grouped into a single level tree
|
||||
structure.
|
||||
|
||||
Secondly, if the `preprocess_filename` param is not None and callable()
|
||||
then the name is passed to that function for preprocessing before being
|
||||
converted to the end location. This is to allow munging of the filename
|
||||
prior to being tested for a blacklist path.
|
||||
|
||||
If any error occurs, then the policy.d directory is cleared, the error is
|
||||
written to the log, and the status line will eventually show as failed.
|
||||
|
||||
@ -653,18 +709,39 @@ def process_policy_resource_file(resource_file,
|
||||
:param template_function: Optional function that can modify the yaml
|
||||
document.
|
||||
:type template_function: Union[None, Callable[[AnyStr], AnyStr]]
|
||||
:param preserve_topdir: Keep the toplevel subdir
|
||||
:type preserve_topdir: bool
|
||||
:param preprocess_filename: Optional function to use to process filenames
|
||||
extracted from the resource file.
|
||||
:type preprocess_filename: Union[None, Callable[[AnyStr]. AnyStr]]
|
||||
:param user: The user to create/write files/directories as
|
||||
:type user: Union[None, str]
|
||||
:param group: the group to create/write files/directories as
|
||||
:type group: Union[None, str]
|
||||
:returns: True if the processing was successful, False if not.
|
||||
:rtype: boolean
|
||||
"""
|
||||
hookenv.log("Running process_policy_resource_file", level=hookenv.DEBUG)
|
||||
blacklist_paths = blacklist_paths or []
|
||||
completed = False
|
||||
_preprocess = None
|
||||
if preprocess_filename is not None and callable(preprocess_filename):
|
||||
_preprocess = preprocess_filename
|
||||
_user = service if user is None else user
|
||||
_group = service if group is None else group
|
||||
try:
|
||||
with open_and_filter_yaml_files(resource_file) as (zfp, gen):
|
||||
with open_and_filter_yaml_files(
|
||||
resource_file, preserve_topdir) as (zfp, gen):
|
||||
# first clear out the policy.d directory and clear success
|
||||
remove_policy_success_file()
|
||||
clean_policyd_dir_for(service, blacklist_paths)
|
||||
clean_policyd_dir_for(service,
|
||||
blacklist_paths,
|
||||
user=_user,
|
||||
group=_group)
|
||||
for name, ext, filename, zipinfo in gen:
|
||||
# See if the name should be preprocessed.
|
||||
if _preprocess is not None:
|
||||
name = _preprocess(name)
|
||||
# construct a name for the output file.
|
||||
yaml_filename = path_for_policy_file(service, name)
|
||||
if yaml_filename in blacklist_paths:
|
||||
@ -682,8 +759,12 @@ def process_policy_resource_file(resource_file,
|
||||
"available".format(filename))
|
||||
doc = template_function(doc)
|
||||
yaml_doc = read_and_validate_yaml(doc, blacklist_keys)
|
||||
with open(yaml_filename, "wt") as f:
|
||||
yaml.dump(yaml_doc, f)
|
||||
# we may have to create the directory
|
||||
maybe_create_directory_for(yaml_filename, _user, _group)
|
||||
ch_host.write_file(yaml_filename,
|
||||
yaml.dump(yaml_doc).encode('utf-8'),
|
||||
_user,
|
||||
_group)
|
||||
# Every thing worked, so we mark up a success.
|
||||
completed = True
|
||||
except (BadZipFile, BadPolicyZipFile, BadPolicyYamlFile) as e:
|
||||
@ -707,10 +788,13 @@ def process_policy_resource_file(resource_file,
|
||||
hookenv.log("Processing {} failed: cleaning policy.d directory"
|
||||
.format(resource_file),
|
||||
level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
clean_policyd_dir_for(service, blacklist_paths)
|
||||
clean_policyd_dir_for(service,
|
||||
blacklist_paths,
|
||||
user=_user,
|
||||
group=_group)
|
||||
else:
|
||||
# touch the success filename
|
||||
hookenv.log("policy.d overrides installed.",
|
||||
level=POLICYD_LOG_LEVEL_DEFAULT)
|
||||
open(_policy_success_file(), "w").close()
|
||||
set_policy_success_file()
|
||||
return completed
|
||||
|
@ -37,9 +37,13 @@ class VaultKVContext(context.OSContextGenerator):
|
||||
)
|
||||
|
||||
def __call__(self):
|
||||
import hvac
|
||||
ctxt = {}
|
||||
# NOTE(hopem): see https://bugs.launchpad.net/charm-helpers/+bug/1849323
|
||||
db = unitdata.kv()
|
||||
last_token = db.get('last-token')
|
||||
# currently known-good secret-id
|
||||
secret_id = db.get('secret-id')
|
||||
|
||||
for relation_id in hookenv.relation_ids(self.interfaces[0]):
|
||||
for unit in hookenv.related_units(relation_id):
|
||||
data = hookenv.relation_get(unit=unit,
|
||||
@ -54,27 +58,48 @@ class VaultKVContext(context.OSContextGenerator):
|
||||
|
||||
# Tokens may change when secret_id's are being
|
||||
# reissued - if so use token to get new secret_id
|
||||
if token != last_token:
|
||||
token_success = False
|
||||
try:
|
||||
secret_id = retrieve_secret_id(
|
||||
url=vault_url,
|
||||
token=token
|
||||
)
|
||||
token_success = True
|
||||
except hvac.exceptions.InvalidRequest:
|
||||
# Try next
|
||||
pass
|
||||
|
||||
if token_success:
|
||||
db.set('secret-id', secret_id)
|
||||
db.set('last-token', token)
|
||||
db.flush()
|
||||
|
||||
ctxt = {
|
||||
'vault_url': vault_url,
|
||||
'role_id': json.loads(role_id),
|
||||
'secret_id': secret_id,
|
||||
'secret_backend': self.secret_backend,
|
||||
}
|
||||
vault_ca = data.get('vault_ca')
|
||||
if vault_ca:
|
||||
ctxt['vault_ca'] = json.loads(vault_ca)
|
||||
self.complete = True
|
||||
return ctxt
|
||||
return {}
|
||||
ctxt['vault_url'] = vault_url
|
||||
ctxt['role_id'] = json.loads(role_id)
|
||||
ctxt['secret_id'] = secret_id
|
||||
ctxt['secret_backend'] = self.secret_backend
|
||||
vault_ca = data.get('vault_ca')
|
||||
if vault_ca:
|
||||
ctxt['vault_ca'] = json.loads(vault_ca)
|
||||
|
||||
self.complete = True
|
||||
break
|
||||
else:
|
||||
if secret_id:
|
||||
ctxt['vault_url'] = vault_url
|
||||
ctxt['role_id'] = json.loads(role_id)
|
||||
ctxt['secret_id'] = secret_id
|
||||
ctxt['secret_backend'] = self.secret_backend
|
||||
vault_ca = data.get('vault_ca')
|
||||
if vault_ca:
|
||||
ctxt['vault_ca'] = json.loads(vault_ca)
|
||||
|
||||
if self.complete:
|
||||
break
|
||||
|
||||
if ctxt:
|
||||
self.complete = True
|
||||
|
||||
return ctxt
|
||||
|
||||
|
||||
def write_vaultlocker_conf(context, priority=100):
|
||||
|
@ -34,6 +34,8 @@ import errno
|
||||
import tempfile
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
from charmhelpers import deprecate
|
||||
|
||||
import six
|
||||
if not six.PY3:
|
||||
from UserDict import UserDict
|
||||
@ -119,19 +121,19 @@ def log(message, level=None):
|
||||
raise
|
||||
|
||||
|
||||
def action_log(message):
|
||||
"""Write an action progress message"""
|
||||
command = ['action-log']
|
||||
def function_log(message):
|
||||
"""Write a function progress message"""
|
||||
command = ['function-log']
|
||||
if not isinstance(message, six.string_types):
|
||||
message = repr(message)
|
||||
command += [message[:SH_MAX_ARG]]
|
||||
# Missing action-log should not cause failures in unit tests
|
||||
# Send action_log output to stderr
|
||||
# Missing function-log should not cause failures in unit tests
|
||||
# Send function_log output to stderr
|
||||
try:
|
||||
subprocess.call(command)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
message = "action-log: {}".format(message)
|
||||
message = "function-log: {}".format(message)
|
||||
print(message, file=sys.stderr)
|
||||
else:
|
||||
raise
|
||||
@ -964,9 +966,23 @@ def charm_dir():
|
||||
return os.environ.get('CHARM_DIR')
|
||||
|
||||
|
||||
def cmd_exists(cmd):
|
||||
"""Return True if the specified cmd exists in the path"""
|
||||
return any(
|
||||
os.access(os.path.join(path, cmd), os.X_OK)
|
||||
for path in os.environ["PATH"].split(os.pathsep)
|
||||
)
|
||||
|
||||
|
||||
@cached
|
||||
@deprecate("moved to function_get()", log=log)
|
||||
def action_get(key=None):
|
||||
"""Gets the value of an action parameter, or all key/value param pairs"""
|
||||
"""
|
||||
.. deprecated:: 0.20.7
|
||||
Alias for :func:`function_get`.
|
||||
|
||||
Gets the value of an action parameter, or all key/value param pairs.
|
||||
"""
|
||||
cmd = ['action-get']
|
||||
if key is not None:
|
||||
cmd.append(key)
|
||||
@ -975,36 +991,103 @@ def action_get(key=None):
|
||||
return action_data
|
||||
|
||||
|
||||
@cached
|
||||
def function_get(key=None):
|
||||
"""Gets the value of an action parameter, or all key/value param pairs"""
|
||||
cmd = ['function-get']
|
||||
# Fallback for older charms.
|
||||
if not cmd_exists('function-get'):
|
||||
cmd = ['action-get']
|
||||
|
||||
if key is not None:
|
||||
cmd.append(key)
|
||||
cmd.append('--format=json')
|
||||
function_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
|
||||
return function_data
|
||||
|
||||
|
||||
@deprecate("moved to function_set()", log=log)
|
||||
def action_set(values):
|
||||
"""Sets the values to be returned after the action finishes"""
|
||||
"""
|
||||
.. deprecated:: 0.20.7
|
||||
Alias for :func:`function_set`.
|
||||
|
||||
Sets the values to be returned after the action finishes.
|
||||
"""
|
||||
cmd = ['action-set']
|
||||
for k, v in list(values.items()):
|
||||
cmd.append('{}={}'.format(k, v))
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def action_fail(message):
|
||||
"""Sets the action status to failed and sets the error message.
|
||||
def function_set(values):
|
||||
"""Sets the values to be returned after the function finishes"""
|
||||
cmd = ['function-set']
|
||||
# Fallback for older charms.
|
||||
if not cmd_exists('function-get'):
|
||||
cmd = ['action-set']
|
||||
|
||||
The results set by action_set are preserved."""
|
||||
for k, v in list(values.items()):
|
||||
cmd.append('{}={}'.format(k, v))
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
@deprecate("moved to function_fail()", log=log)
|
||||
def action_fail(message):
|
||||
"""
|
||||
.. deprecated:: 0.20.7
|
||||
Alias for :func:`function_fail`.
|
||||
|
||||
Sets the action status to failed and sets the error message.
|
||||
|
||||
The results set by action_set are preserved.
|
||||
"""
|
||||
subprocess.check_call(['action-fail', message])
|
||||
|
||||
|
||||
def function_fail(message):
|
||||
"""Sets the function status to failed and sets the error message.
|
||||
|
||||
The results set by function_set are preserved."""
|
||||
cmd = ['function-fail']
|
||||
# Fallback for older charms.
|
||||
if not cmd_exists('function-fail'):
|
||||
cmd = ['action-fail']
|
||||
cmd.append(message)
|
||||
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def action_name():
|
||||
"""Get the name of the currently executing action."""
|
||||
return os.environ.get('JUJU_ACTION_NAME')
|
||||
|
||||
|
||||
def function_name():
|
||||
"""Get the name of the currently executing function."""
|
||||
return os.environ.get('JUJU_FUNCTION_NAME') or action_name()
|
||||
|
||||
|
||||
def action_uuid():
|
||||
"""Get the UUID of the currently executing action."""
|
||||
return os.environ.get('JUJU_ACTION_UUID')
|
||||
|
||||
|
||||
def function_id():
|
||||
"""Get the ID of the currently executing function."""
|
||||
return os.environ.get('JUJU_FUNCTION_ID') or action_uuid()
|
||||
|
||||
|
||||
def action_tag():
|
||||
"""Get the tag for the currently executing action."""
|
||||
return os.environ.get('JUJU_ACTION_TAG')
|
||||
|
||||
|
||||
def function_tag():
|
||||
"""Get the tag for the currently executing function."""
|
||||
return os.environ.get('JUJU_FUNCTION_TAG') or action_tag()
|
||||
|
||||
|
||||
def status_set(workload_state, message):
|
||||
"""Set the workload state with a message
|
||||
|
||||
|
@ -210,6 +210,7 @@ def install():
|
||||
apt_install(pkgs, fatal=True)
|
||||
initialize_ufw()
|
||||
ensure_swift_directories()
|
||||
install_vaultlocker()
|
||||
|
||||
|
||||
@hooks.hook('config-changed')
|
||||
@ -433,8 +434,11 @@ def main():
|
||||
except UnregisteredHookError as e:
|
||||
log('Unknown hook {} - skipping.'.format(e))
|
||||
required_interfaces = copy.deepcopy(REQUIRED_INTERFACES)
|
||||
if config('encrypt'):
|
||||
|
||||
if config('encrypt') and \
|
||||
len(filter_installed_packages('vaultlocker')) == 0:
|
||||
required_interfaces['vault'] = ['secrets-storage']
|
||||
|
||||
set_os_workload_status(CONFIGS, required_interfaces,
|
||||
charm_func=assess_status)
|
||||
os_application_version_set(VERSION_PACKAGE)
|
||||
|
@ -29,6 +29,7 @@ from charmhelpers.fetch import (
|
||||
apt_purge,
|
||||
apt_autoremove,
|
||||
filter_missing_packages,
|
||||
filter_installed_packages,
|
||||
)
|
||||
|
||||
from charmhelpers.core.unitdata import (
|
||||
@ -190,6 +191,10 @@ def ensure_swift_directories():
|
||||
if not os.path.isdir(d)]
|
||||
|
||||
|
||||
def vaultlocker_installed():
|
||||
return len(filter_installed_packages(['vaultlocker'])) == 0
|
||||
|
||||
|
||||
def register_configs():
|
||||
release = get_os_codename_package('swift', fatal=False) or 'essex'
|
||||
configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
|
||||
@ -200,12 +205,18 @@ def register_configs():
|
||||
[RsyncContext(), SwiftStorageServerContext()])
|
||||
# NOTE: add VaultKVContext so interface status can be assessed
|
||||
for server in ['account', 'object', 'container']:
|
||||
configs.register('/etc/swift/%s-server.conf' % server,
|
||||
[SwiftStorageServerContext(),
|
||||
context.BindHostContext(),
|
||||
context.WorkerConfigContext(),
|
||||
vaultlocker.VaultKVContext(
|
||||
vaultlocker.VAULTLOCKER_BACKEND)]),
|
||||
contexts = [SwiftStorageServerContext(),
|
||||
context.BindHostContext(),
|
||||
context.WorkerConfigContext()]
|
||||
|
||||
# if vault deps are not installed it is not yet possible to check the
|
||||
# vault context status since it requires the hvac dependency.
|
||||
if vaultlocker_installed():
|
||||
contexts.append(vaultlocker.VaultKVContext(
|
||||
vaultlocker.VAULTLOCKER_BACKEND))
|
||||
|
||||
configs.register('/etc/swift/%s-server.conf' % server, contexts)
|
||||
|
||||
return configs
|
||||
|
||||
|
||||
@ -524,18 +535,26 @@ def ensure_devs_tracked():
|
||||
|
||||
def setup_storage(encrypt=False):
|
||||
# Preflight check vault relation if encryption is enabled
|
||||
vault_kv = vaultlocker.VaultKVContext(vaultlocker.VAULTLOCKER_BACKEND)
|
||||
context = vault_kv()
|
||||
if encrypt and not vault_kv.complete:
|
||||
log("Encryption requested but vault relation not complete",
|
||||
level=DEBUG)
|
||||
return
|
||||
elif encrypt and vault_kv.complete:
|
||||
# NOTE: only write vaultlocker configuration once relation is complete
|
||||
# otherwise we run the chance of an empty configuration file
|
||||
# being installed on a machine with other vaultlocker based
|
||||
# services
|
||||
vaultlocker.write_vaultlocker_conf(context, priority=90)
|
||||
if encrypt:
|
||||
if not vaultlocker_installed():
|
||||
log("Encryption requested but vaultlocker not yet installed",
|
||||
level=DEBUG)
|
||||
return
|
||||
|
||||
vault_kv = vaultlocker.VaultKVContext(
|
||||
secret_backend=vaultlocker.VAULTLOCKER_BACKEND
|
||||
)
|
||||
context = vault_kv()
|
||||
if vault_kv.complete:
|
||||
# NOTE: only write vaultlocker configuration once relation is
|
||||
# complete otherwise we run the chance of an empty
|
||||
# configuration file being installed on a machine with other
|
||||
# vaultlocker based services
|
||||
vaultlocker.write_vaultlocker_conf(context, priority=90)
|
||||
else:
|
||||
log("Encryption requested but vault relation not complete",
|
||||
level=DEBUG)
|
||||
return
|
||||
|
||||
# Ensure /srv/node exists just in case no disks
|
||||
# are detected and used.
|
||||
|
@ -316,6 +316,7 @@ class SwiftStorageUtilsTests(CharmTestCase):
|
||||
swift_utils.setup_storage()
|
||||
self.assertEqual(self.check_call.call_count, 0)
|
||||
|
||||
@patch.object(swift_utils, 'filter_installed_packages')
|
||||
@patch.object(swift_utils, "uuid")
|
||||
@patch.object(swift_utils, "vaultlocker")
|
||||
@patch.object(swift_utils.charmhelpers.core.fstab, "Fstab")
|
||||
@ -326,7 +327,9 @@ class SwiftStorageUtilsTests(CharmTestCase):
|
||||
@patch.object(swift_utils, 'get_device_blkid')
|
||||
def test_setup_storage_encrypt(self, mock_get_device_blkid, determine,
|
||||
mkfs, clean, mock_is_device_in_ring,
|
||||
mock_Fstab, mock_vaultlocker, mock_uuid):
|
||||
mock_Fstab, mock_vaultlocker, mock_uuid,
|
||||
filter_installed_packages):
|
||||
filter_installed_packages.return_value = []
|
||||
mock_context = MagicMock()
|
||||
mock_context.complete = True
|
||||
mock_context.return_value = 'test_context'
|
||||
@ -439,6 +442,7 @@ class SwiftStorageUtilsTests(CharmTestCase):
|
||||
renderer.assert_called_with(templates_dir=swift_utils.TEMPLATES,
|
||||
openstack_release='essex')
|
||||
|
||||
@patch.object(swift_utils, 'filter_installed_packages')
|
||||
@patch('charmhelpers.contrib.openstack.context.WorkerConfigContext')
|
||||
@patch('charmhelpers.contrib.openstack.context.BindHostContext')
|
||||
@patch.object(swift_utils, 'SwiftStorageContext')
|
||||
@ -447,7 +451,9 @@ class SwiftStorageUtilsTests(CharmTestCase):
|
||||
@patch('charmhelpers.contrib.openstack.templating.OSConfigRenderer')
|
||||
def test_register_configs_post_install(self, renderer,
|
||||
swift, rsync, server,
|
||||
bind_context, worker_context):
|
||||
bind_context, worker_context,
|
||||
filter_installed_packages):
|
||||
filter_installed_packages.return_value = []
|
||||
swift.return_value = 'swift_context'
|
||||
rsync.return_value = 'rsync_context'
|
||||
server.return_value = 'swift_server_context'
|
||||
|
Loading…
x
Reference in New Issue
Block a user