diff --git a/README.rst b/README.rst index a8d4854..7a79659 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,9 @@ snap.openstack Helpers for writing Snaps for OpenStack -Please fill here a long description which must be at least 3 lines wrapped on -80 cols, so that distribution package maintainers can use it in their packages. -Note that this is a hard requirement. +This project provides a wrapper for automatically wrapping openstack +commands in snaps, building out appropriate Oslo configuration and +logging options on the command line. * Free software: Apache license * Documentation: http://docs.openstack.org/developer/snap.openstack @@ -16,4 +16,4 @@ Note that this is a hard requirement. Features -------- -* TODO +* Support for classic mode snap use diff --git a/doc/source/snap-openstack.yaml b/doc/source/snap-openstack.yaml new file mode 100644 index 0000000..6e2e05c --- /dev/null +++ b/doc/source/snap-openstack.yaml @@ -0,0 +1,45 @@ +# Sample snap-openstack configuration file +# +# snap-openstack will automatically substitute the following +# in both paths for files and directories and in templates: +# +# SNAP_COMMON -> snap_common +# SNAP -> snap +# SNAP -> snap_shared +# +# Setup is executed for all entry points prior to execution +# snap-openstack will assure that templated files are in place +# and that any directory structure in $SNAP_COMMON is created +setup: + dirs: + - "{snap_common}/etc/nova.conf.d" + - "{snap_common}/etc/nova" + - "{snap_common}/logs" + templates: + "nova-snap.conf.j2": "[snap_common}/etc/nova.conf.d/nova-snap.conf" +# Entry points are used to execute commands from with the snap +# with a sane set of defaults in terms of configuration files +# and directories +entry_points: + # Executes the following: + # + # nova-manage --config-file=$SNAP/etc/nova/nova,conf \ + # --config-file=$SNAP_COMMON/etc/nova/nova.conf \ + # --config-dir=$SNAP_COMMON/etc/nova.conf.d \ + # --log-file=$SNAP_COMMON/logs/nova-manage.log + # + # this is designed to be executed from the snapcraft.yaml apps section + # using: + # + # command: snap-openstack nova-manage + # + # any additional arguments will be passed to the underlying binary + nova-manage: + binary: nova-manage + config-files: + - "{snap}/etc/nova/nova.conf" + - "[snap_common}/etc/nova/nova.conf" + config-dirs: + - "{snap_common}/etc/nova.conf.d" + log-file: + - "{snap_common}/logs/nova-manage.log" diff --git a/requirements.txt b/requirements.txt index 95d0fe8..4d8d2bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,6 @@ # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 + +# Left unversioned as designed to align with OpenStack component being snapped +jinja2 diff --git a/setup.cfg b/setup.cfg index ac2ecc5..4b0328d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,4 +48,8 @@ output_file = snap_openstack/locale/snap_openstack.pot [build_releasenotes] all_files = 1 build-dir = releasenotes/build -source-dir = releasenotes/source \ No newline at end of file +source-dir = releasenotes/source + +[entry_points] +console_scripts = + snap-openstack = snap_openstack.cmd.run:main diff --git a/snap_openstack/base.py b/snap_openstack/base.py new file mode 100644 index 0000000..34799f7 --- /dev/null +++ b/snap_openstack/base.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python + +# Copyright 2016 Canonical UK Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import subprocess +import yaml +import logging + +from snap_openstack.renderer import SnapFileRenderer + +LOG = logging.getLogger(__name__) + + +SNAP_ENV = ['SNAP_NAME', + 'SNAP_VERSION', + 'SNAP_REVISION', + 'SNAP_ARCH', + 'SNAP_LIBRARY_PATH', + 'SNAP', + 'SNAP_DATA', + 'SNAP_COMMON', + 'SNAP_USER_DATA', + 'SNAP_USER_COMMON', + 'TMPDIR'] + + +def snap_env(): + '''Grab SNAP* environment variables + + @return dict of all SNAP* environment variables indexed in lower case + ''' + _env = {} + for key in SNAP_ENV: + _env[key.lower()] = os.environ.get(key) + return _env + + +def ensure_dir(filepath): + '''Ensure that the directory structure to support a give file path exists''' + dir_name = os.path.dirname(filepath) + if not os.path.exists(dir_name): + LOG.info('Creating directory {}'.format(dir_name)) + os.makedirs(dir_name, 0o750) + + +class OpenStackSnap(): + '''Main executor class for snap-openstack''' + + def __init__(self, config_file): + with open(config_file, 'r') as config: + self.configuration = yaml.load(config) + self.snap_env = snap_env() + + def setup(self): + '''Perform any pre-execution snap setup + + Run this method prior to use of the execute metho + ''' + setup = self.configuration['setup'] + renderer = SnapFileRenderer() + + for dirs in setup['dirs']: + dir_name = dirs.format(self.snap_env) + ensure_dir(dir_name) + + for template in setup['templates']: + target = setup['templates'][template] + target_file = target.format(self.snap_env) + ensure_dir(target_file) + LOG.info('Rendering {} to {}'.format(template, + target_file)) + with open(target_file, 'w') as tf: + os.fchmod(tf.fileno(), 0o550) + tf.write(renderer.render(template, + self.snap_env)) + + def execute(self, argv): + '''Execute snap command building out configuration and log options''' + entry_point = self.configuration['entry_points'].get(argv[1]) + if not entry_point: + _msg = 'Enable to find entry point for {}'.format(argv[1]) + LOG.error(_msg) + raise ValueError(_msg) + + other_args = argv[:2] + # Build out command to run + cmd = [entry_point['binary']] + + for cfile in entry_point.get('config-files', []): + cfile = cfile.format(self.snap_env) + if os.path.exists(cfile): + cmd.append('--config-file={}'.format(cfile)) + else: + LOG.warning('Configuration file {} not found' + ', skipping'.format(cfile)) + + for cdir in entry_point.get('config-dirs', []): + cdir = cdir.format(self.snap_env) + if os.path.exists(cdir): + cmd.append('--config-dir={}'.format(cdir)) + else: + LOG.warning('Configuration directory {} not found' + ', skipping'.format(cdir)) + + log_file = entry_point.get('log-file') + if log_file: + log_file = log_file.format(self.snap_env) + cmd.append('--log-file={}'.format(log_file)) + + # Ensure any arguments passed to wrapper are propagated + cmd.extend(other_args) + subprocess.check_call(cmd) diff --git a/snap_openstack/cmd/__init__.py b/snap_openstack/cmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/snap_openstack/cmd/run.py b/snap_openstack/cmd/run.py new file mode 100644 index 0000000..5f5c687 --- /dev/null +++ b/snap_openstack/cmd/run.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +# Copyright 2016 Canonical UK Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import sys +import logging + +from snap_openstack.base import OpenStackSnap + +LOG = logging.getLogger(__name__) + +CONFIG_FILE = 'snap-openstack.yaml' + +def main(): + logging.basicConfig(level=logging.INFO) + snap = os.environ.get('SNAP') + if not snap: + LOG.error('Not executing in snap environment, exiting') + sys.exit(1) + config_path = os.path.join(snap, + CONFIG_FILE) + if os.path.exists(config_path): + LOG.info('Using snap wrapper: {}'.format(config_path)) + s_openstack = OpenStackSnap(config_path) + s_openstack.setup() + s_openstack.execute(sys.argv) + else: + LOG.error('Unable to find snap-openstack.yaml configuration file') + sys.exit(1) + diff --git a/snap_openstack/renderer.py b/snap_openstack/renderer.py new file mode 100644 index 0000000..47735c8 --- /dev/null +++ b/snap_openstack/renderer.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# Copyright 2016 Canonical UK Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import logging + +from jinja2 import FileSystemLoader, Environment, exceptions + +LOG = logging.getLogger(__name__) + + +class SnapFileRenderer(): + '''Helper class for rendering snap templates for runtime use''' + + def __init__(self): + self._loaders = [ + FileSystemLoader(os.path.join(os.environ.get('SNAP'), + 'templates')) + ] + self._tmpl_env = Environment(loader=self._loaders) + + def render(self, template_name, env): + '''Render j2 template using SNAP environment context + + @param template_name: name of the template to use for rendering + @return: string of rendered context, ready to write back to a file + ''' + try: + template = self._tmpl_env.get_template(template_name) + except exceptions.TemplateNotFound as te: + LOG.error('Unable to locate template: {}'.format(template_name)) + raise te + return template.render(env) \ No newline at end of file