From e88a960ea9c4cf2059c09cf8f2508e9769cc8de3 Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Fri, 4 Sep 2015 15:11:42 -0400 Subject: [PATCH 01/14] Refactor giftwrap builders Previously the two builders had operated as separate build procesess. What this meant is that when one builder would change, the other would also need to change in order to support build parity. This change moves all of the build logic into the base Builder abstract class. Each sub Builder then implements the necessary primitives to support the build steps outlined by the base class. --- giftwrap/builder.py | 54 --------- giftwrap/builders/__init__.py | 157 +++++++++++++++++++++++++++ giftwrap/builders/docker_builder.py | 109 ++++++++++--------- giftwrap/builders/package_builder.py | 133 ++++++++++------------- giftwrap/gerrit.py | 3 +- giftwrap/openstack_project.py | 4 +- giftwrap/shell.py | 8 +- giftwrap/templates/Dockerfile.jinja2 | 4 +- 8 files changed, 279 insertions(+), 193 deletions(-) delete mode 100644 giftwrap/builder.py diff --git a/giftwrap/builder.py b/giftwrap/builder.py deleted file mode 100644 index d9403cf..0000000 --- a/giftwrap/builder.py +++ /dev/null @@ -1,54 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2014, Craig Tracey -# All Rights Reserved. -# -# 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 - -import logging - -LOG = logging.getLogger(__name__) - - -class Builder(object): - - def __init__(self, spec): - self._spec = spec - self.settings = spec.settings - - def _validate_settings(self): - raise NotImplementedError() - - def _build(self): - raise NotImplementedError() - - def _cleanup(self): - raise NotImplementedError() - - def build(self): - self._validate_settings() - self._build() - - def cleanup(self): - self._cleanup() - - -from giftwrap.builders.package_builder import PackageBuilder -from giftwrap.builders.docker_builder import DockerBuilder - - -def create_builder(spec): - if spec.settings.build_type == 'package': - return PackageBuilder(spec) - elif spec.settings.build_type == 'docker': - return DockerBuilder(spec) - raise Exception("Unknown build_type: '%s'", spec.settings.build_type) diff --git a/giftwrap/builders/__init__.py b/giftwrap/builders/__init__.py index e69de29..666507a 100644 --- a/giftwrap/builders/__init__.py +++ b/giftwrap/builders/__init__.py @@ -0,0 +1,157 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2014, Craig Tracey +# All Rights Reserved. +# +# 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 + +import logging +import os + +from giftwrap.gerrit import GerritReview + +from abc import abstractmethod, ABCMeta + +LOG = logging.getLogger(__name__) + + +class Builder(object): + __metaclass__ = ABCMeta + + def __init__(self, spec): + self._temp_dir = None + self._temp_src_dir = None + self._spec = spec + + def _get_venv_pip_path(self, venv_path): + return os.path.join(venv_path, 'bin/pip') + + def _get_gerrit_dependencies(self, repo, project): + try: + review = GerritReview(repo.head.change_id, project.git_path) + return review.build_pip_dependencies(string=True) + except Exception as e: + LOG.warning("Could not install gerrit dependencies!!! " + "Error was: %s", e) + return "" + + def _build_project(self, project): + self._prepare_project_build(project) + self._make_dir(project.install_path) + + # clone the source + src_clone_dir = os.path.join(self._temp_src_dir, project.name) + repo = self._clone_project(project.giturl, project.name, + project.gitref, project.gitdepth, + src_clone_dir) + + # create and build the virtualenv + self._create_virtualenv(project.venv_command, project.install_path) + dependencies = "" + if project.pip_dependencies: + dependencies = " ".join(project.pip_dependencies) + if self._spec.settings.gerrit_dependencies: + dependencies = "%s %s" % (dependencies, + self._get_gerrit_dependencies(repo, + project)) + if len(dependencies): + self._install_pip_dependencies(project.install_path, dependencies) + + if self._spec.settings.include_config: + self._copy_sample_config(src_clone_dir, project) + + self._install_project(project.install_path, src_clone_dir) + + # finish up + self._finalize_project_build(project) + + def build(self): + spec = self._spec + + self._prepare_build() + + # Create a temporary directory for the source code + self._temp_dir = self._make_temp_dir() + self._temp_src_dir = os.path.join(self._temp_dir, 'src') + LOG.debug("Temporary working directory: %s", self._temp_dir) + + for project in spec.projects: + self._build_project(project) + + self._finalize_build() + + def cleanup(self): + self._cleanup_build() + + @abstractmethod + def _execute(self, command, cwd=None, exit=0): + return + + @abstractmethod + def _make_temp_dir(self, prefix='giftwrap'): + return + + @abstractmethod + def _make_dir(self, path, mode=0777): + return + + @abstractmethod + def _prepare_build(self): + return + + @abstractmethod + def _prepare_project_build(self, project): + return + + @abstractmethod + def _clone_project(self, project): + return + + @abstractmethod + def _create_virtualenv(self, venv_command, path): + return + + @abstractmethod + def _install_pip_dependencies(self, venv_path, dependencies): + return + + @abstractmethod + def _copy_sample_config(self, src_clone_dir, project): + return + + @abstractmethod + def _install_project(self, venv_path, src_clone_dir): + return + + @abstractmethod + def _finalize_project_build(self, project): + return + + @abstractmethod + def _finalize_build(self): + return + + @abstractmethod + def _cleanup_build(self): + return + + +from giftwrap.builders.package_builder import PackageBuilder # noqa +from giftwrap.builders.docker_builder import DockerBuilder # noqa + + +class BuilderFactory: + + @staticmethod + def create_builder(builder_type, build_spec): + targetclass = "%sBuilder" % builder_type.capitalize() + return globals()[targetclass](build_spec) diff --git a/giftwrap/builders/docker_builder.py b/giftwrap/builders/docker_builder.py index f892995..8d5b836 100644 --- a/giftwrap/builders/docker_builder.py +++ b/giftwrap/builders/docker_builder.py @@ -22,7 +22,7 @@ import os import re import tempfile -from giftwrap.builder import Builder +from giftwrap.builders import Builder LOG = logging.getLogger(__name__) @@ -41,7 +41,6 @@ APT_REQUIRED_PACKAGES = [ 'libssl-dev', 'python-dev', 'libmysqlclient-dev', - 'python-virtualenv', 'python-pip', 'build-essential' ] @@ -51,61 +50,71 @@ DEFAULT_SRC_PATH = '/opt/openstack' class DockerBuilder(Builder): def __init__(self, spec): - self.template = DEFAULT_TEMPLATE_FILE self.base_image = 'ubuntu:12.04' self.maintainer = 'maintainer@example.com' self.envvars = {'DEBIAN_FRONTEND': 'noninteractive'} - self._paths = [] + self._commands = [] super(DockerBuilder, self).__init__(spec) - def _validate_settings(self): - pass + def _execute(self, command, cwd=None, exit=0): + if cwd: + self._commands.append("cd %s" % (cwd)) + self._commands.append(command) + if cwd: + self._commands.append("cd -") - def _cleanup(self): - pass + def _make_temp_dir(self, prefix='giftwrap'): + return "/tmp/giftwrap" + self._commands.append("mktemp -d -t %s.XXXXXXXXXX" % prefix) - def _get_prep_commands(self): - commands = [] - commands.append('apt-get update && apt-get install -y %s' % - ' '.join(APT_REQUIRED_PACKAGES)) - return commands + def _make_dir(self, path, mode=0777): + self._commands.append("mkdir -p -m %o %s" % (mode, path)) - def _get_build_commands(self, src_path): - commands = [] - commands.append('mkdir -p %s' % src_path) + def _prepare_project_build(self, project): + return - for project in self._spec.projects: - if project.system_dependencies: - commands.append('apt-get update && apt-get install -y %s' % - ' '.join(project.system_dependencies)) + def _clone_project(self, giturl, name, gitref, depth, path): + cmd = "git clone %s -b %s --depth=%d %s" % (giturl, gitref, + depth, path) + self._commands.append(cmd) - project_src_path = os.path.join(src_path, project.name) - commands.append('git clone --depth 1 %s -b %s %s' % - (project.giturl, project.gitref, project_src_path)) - commands.append('COMMIT=`git rev-parse HEAD` && echo "%s $COMMIT" ' - '> %s/gitinfo' % (project.giturl, - project.install_path)) - commands.append('mkdir -p %s' % - os.path.dirname(project.install_path)) - commands.append('virtualenv --system-site-packages %s' % - project.install_path) + def _create_virtualenv(self, venv_command, path): + self._execute(venv_command, path) - project_bin_path = os.path.join(project.install_path, 'bin') - self._paths.append(project_bin_path) - venv_pip_path = os.path.join(project_bin_path, 'pip') + def _install_pip_dependencies(self, venv_path, dependencies): + pip_path = self._get_venv_pip_path(venv_path) + self._execute("%s install %s" % (pip_path, dependencies)) - if project.pip_dependencies: - commands.append("%s install %s" % (venv_pip_path, - ' '.join(project.pip_dependencies))) - commands.append("%s install %s" % (venv_pip_path, - project_src_path)) + def _copy_sample_config(self, src_clone_dir, project): + src_config = os.path.join(src_clone_dir, 'etc') + dest_config = os.path.join(project.install_path, 'etc') - return commands + self._commands.append("if [ -d %s ]; then cp -R %s %s; fi" % ( + src_config, src_config, dest_config)) - def _get_cleanup_commands(self, src_path): - commands = [] - commands.append('rm -rf %s' % src_path) - return commands + def _install_project(self, venv_path, src_clone_dir): + pip_path = self._get_venv_pip_path(venv_path) + self._execute("%s install %s" % (pip_path, src_clone_dir)) + + def _finalize_project_build(self, project): + self._commands.append("rm -rf %s" % self._temp_dir) + for command in self._commands: + print command + + def _finalize_build(self): + template_vars = { + 'commands': self._commands + } + print self._render_dockerfile(template_vars) + self._build_image() + + def _cleanup_build(self): + return + + def _prepare_build(self): + self._commands.append('apt-get update && apt-get install -y %s' % + ' '.join(APT_REQUIRED_PACKAGES)) + self._commands.append("pip install -U pip virtualenv") def _set_path(self): path = ":".join(self._paths) @@ -116,16 +125,14 @@ class DockerBuilder(Builder): template_vars.update(extra_vars) template_loader = jinja2.FileSystemLoader(searchpath='/') template_env = jinja2.Environment(loader=template_loader) - template = template_env.get_template(self.template) + template = template_env.get_template(DEFAULT_TEMPLATE_FILE) return template.render(template_vars) - def _build(self): - src_path = DEFAULT_SRC_PATH - commands = self._get_prep_commands() - commands += self._get_build_commands(src_path) - commands += self._get_cleanup_commands(src_path) - self._set_path() - dockerfile_contents = self._render_dockerfile(locals()) + def _build_image(self): + template_vars = { + 'commands': self._commands + } + dockerfile_contents = self._render_dockerfile(template_vars) tempdir = tempfile.mkdtemp() dockerfile = os.path.join(tempdir, 'Dockerfile') diff --git a/giftwrap/builders/package_builder.py b/giftwrap/builders/package_builder.py index 530808e..b25c1c1 100644 --- a/giftwrap/builders/package_builder.py +++ b/giftwrap/builders/package_builder.py @@ -20,8 +20,7 @@ import os import shutil import tempfile -from giftwrap.builder import Builder -from giftwrap.gerrit import GerritReview +from giftwrap.builders import Builder from giftwrap.openstack_git_repo import OpenstackGitRepo from giftwrap.package import Package from giftwrap.util import execute @@ -32,95 +31,73 @@ LOG = logging.getLogger(__name__) class PackageBuilder(Builder): def __init__(self, spec): - self._tempdir = None + self._temp_dir = None super(PackageBuilder, self).__init__(spec) - def _validate_settings(self): - pass + def _execute(self, command, cwd=None, exit=0): + return execute(command, cwd, exit) - def _install_gerrit_dependencies(self, repo, project, install_path): - try: - review = GerritReview(repo.head.change_id, project.git_path) - LOG.info("Installing '%s' pip dependencies to the virtualenv", - project.name) - execute(project.install_command % - review.build_pip_dependencies(string=True), install_path) - except Exception as e: - LOG.warning("Could not install gerrit dependencies!!! " - "Error was: %s", e) + def _make_temp_dir(self, prefix='giftwrap'): + return tempfile.mkdtemp(prefix) - def _build(self): - spec = self._spec + def _make_dir(self, path, mode=0777): + os.makedirs(path, mode) - self._tempdir = tempfile.mkdtemp(prefix='giftwrap') - src_path = os.path.join(self._tempdir, 'src') - LOG.debug("Temporary working directory: %s", self._tempdir) + def _prepare_build(self): + return - for project in spec.projects: - LOG.info("Beginning to build '%s'", project.name) + def _prepare_project_build(self, project): + install_path = project.install_path - install_path = project.install_path - LOG.debug("Installing '%s' to '%s'", project.name, install_path) + LOG.info("Beginning to build '%s'", project.name) + if os.path.exists(install_path): + if self._spec.settings.force_overwrite: + LOG.info("force_overwrite is set, so removing " + "existing path '%s'" % install_path) + shutil.rmtree(install_path) + else: + raise Exception("Install path '%s' already exists" % + install_path) - # if anything is in our way, see if we can get rid of it - if os.path.exists(install_path): - if spec.settings.force_overwrite: - LOG.info("force_overwrite is set, so removing " - "existing path '%s'" % install_path) - shutil.rmtree(install_path) - else: - raise Exception("Install path '%s' already exists" % - install_path) - os.makedirs(install_path) + def _clone_project(self, giturl, name, gitref, depth, path): + LOG.info("Fetching source code for '%s'", name) + repo = OpenstackGitRepo(giturl, name, gitref, depth) + repo.clone(path) + return repo - # clone the project's source to a temporary directory - project_src_path = os.path.join(src_path, project.name) - os.makedirs(project_src_path) + def _create_virtualenv(self, venv_command, path): + self._execute(venv_command, path) - LOG.info("Fetching source code for '%s'", project.name) - repo = OpenstackGitRepo(project.giturl, project.name, - project.gitref, - depth=project.gitdepth) - repo.clone(project_src_path) + def _install_pip_dependencies(self, venv_path, dependencies): + pip_path = self._get_venv_pip_path(venv_path) + self._execute("%s install %s" % (pip_path, dependencies)) - # tell package users where this came from - gitinfo_file = os.path.join(install_path, 'gitinfo') - with open(gitinfo_file, 'w') as fh: - fh.write("%s %s" % (project.giturl, repo.head.hexsha)) + def _copy_sample_config(self, src_clone_dir, project): + src_config = os.path.join(src_clone_dir, 'etc') + dest_config = os.path.join(project.install_path, 'etc') - # start building the virtualenv for the project - LOG.info("Creating the virtualenv for '%s'", project.name) - execute(project.venv_command, install_path) + if not os.path.exists(src_config): + LOG.warning("Project configuration does not seem to exist " + "in source repo '%s'. Skipping.", project.name) + else: + LOG.debug("Copying config from '%s' to '%s'", src_config, + dest_config) + distutils.dir_util.copy_tree(src_config, dest_config) - # install into the virtualenv - LOG.info("Installing '%s' to the virtualenv", project.name) - venv_pip_path = os.path.join(install_path, 'bin/pip') + def _install_project(self, venv_path, src_clone_dir): + pip_path = self._get_venv_pip_path(venv_path) + self._execute("%s install %s" % (pip_path, src_clone_dir)) - deps = " ".join(project.pip_dependencies) - execute("%s install %s" % (venv_pip_path, deps)) + def _finalize_project_build(self, project): + # build the package + pkg = Package(project.package_name, project.version, + project.install_path, self._spec.settings.output_dir, + self._spec.settings.force_overwrite, + project.system_dependencies) + pkg.build() - if spec.settings.include_config: - src_config = os.path.join(project_src_path, 'etc') - dest_config = os.path.join(install_path, 'etc') - if not os.path.exists(src_config): - LOG.warning("Project configuration does not seem to exist " - "in source repo '%s'. Skipping.", project.name) - else: - LOG.debug("Copying config from '%s' to '%s'", src_config, - dest_config) - distutils.dir_util.copy_tree(src_config, dest_config) + def _finalize_build(self): + return - if spec.settings.gerrit_dependencies: - self._install_gerrit_dependencies(repo, project, install_path) - - execute("%s install %s" % (venv_pip_path, project_src_path)) - - # now build the package - pkg = Package(project.package_name, project.version, - install_path, spec.settings.output_dir, - spec.settings.force_overwrite, - project.system_dependencies) - pkg.build() - - def _cleanup(self): - shutil.rmtree(self._tempdir) + def _cleanup_build(self): + shutil.rmtree(self._temp_dir) diff --git a/giftwrap/gerrit.py b/giftwrap/gerrit.py index 042989e..f9309c5 100644 --- a/giftwrap/gerrit.py +++ b/giftwrap/gerrit.py @@ -50,7 +50,8 @@ class GerritReview(object): freeze_found = True continue elif re.match('[\w\-]+==.+', line) and not line.startswith('-e'): - dependencies.append(line) + dependency = line.split('#')[0].strip() # remove any comments + dependencies.append(dependency) short_name = self.project.split('/')[1] dependencies = filter(lambda x: not x.startswith(short_name + "=="), diff --git a/giftwrap/openstack_project.py b/giftwrap/openstack_project.py index 3a41868..ff7ef38 100644 --- a/giftwrap/openstack_project.py +++ b/giftwrap/openstack_project.py @@ -23,7 +23,7 @@ DEFAULT_GITURL = { 'openstack': 'https://git.openstack.org/openstack/', 'stackforge': 'https://github.com/stackforge/' } -DEFAULT_VENV_COMMAND = "virtualenv ." +DEFAULT_VENV_COMMAND = "virtualenv --no-wheel ." DEFAULT_INSTALL_COMMAND = "./bin/pip install %s" # noqa TEMPLATE_VARS = ('name', 'version', 'gitref', 'stackforge') @@ -32,7 +32,7 @@ TEMPLATE_VARS = ('name', 'version', 'gitref', 'stackforge') class OpenstackProject(object): def __init__(self, settings, name, version=None, gitref=None, giturl=None, - gitdepth=None, venv_command=None, install_command=None, + gitdepth=1, venv_command=None, install_command=None, install_path=None, package_name=None, stackforge=False, system_dependencies=[], pip_dependencies=[]): self._settings = settings diff --git a/giftwrap/shell.py b/giftwrap/shell.py index bd2e89f..a461fed 100644 --- a/giftwrap/shell.py +++ b/giftwrap/shell.py @@ -19,8 +19,7 @@ import logging import signal import sys -import giftwrap.builder - +from giftwrap.builders import BuilderFactory from giftwrap.build_spec import BuildSpec from giftwrap.color import ColorStreamHandler @@ -48,7 +47,7 @@ def build(args): manifest = fh.read() buildspec = BuildSpec(manifest, args.version, args.type) - builder = giftwrap.builder.create_builder(buildspec) + builder = BuilderFactory.create_builder(args.type, buildspec) def _signal_handler(*args): LOG.info("Process interrrupted. Cleaning up.") @@ -79,7 +78,8 @@ def main(): description='build giftwrap packages') build_subcmd.add_argument('-m', '--manifest', required=True) build_subcmd.add_argument('-v', '--version') - build_subcmd.add_argument('-t', '--type', choices=('docker', 'package')) + build_subcmd.add_argument('-t', '--type', choices=('docker', 'package'), + required=True) build_subcmd.set_defaults(func=build) args = parser.parse_args() diff --git a/giftwrap/templates/Dockerfile.jinja2 b/giftwrap/templates/Dockerfile.jinja2 index bb90490..13374c6 100644 --- a/giftwrap/templates/Dockerfile.jinja2 +++ b/giftwrap/templates/Dockerfile.jinja2 @@ -7,6 +7,4 @@ MAINTAINER {{ maintainer }} ENV {{ k }} {{ v }} {% endfor -%} -{% for command in commands -%} -RUN {{ command }} -{% endfor %} +RUN {% for command in commands[:-1] -%}{{ command|safe }} && {% endfor -%} {{ commands[-1]|safe }} From 3c864f4ea748e3ed1763bda645f7740f454a5d90 Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Sat, 19 Sep 2015 16:04:38 -0500 Subject: [PATCH 02/14] docker client was trying to use old API version also needed to pass project details to build to be able to name the image. --- giftwrap/builders/__init__.py | 4 ++-- giftwrap/builders/docker_builder.py | 15 ++++++++------- giftwrap/builders/package_builder.py | 2 +- giftwrap/shell.py | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/giftwrap/builders/__init__.py b/giftwrap/builders/__init__.py index 666507a..1d3d6cd 100644 --- a/giftwrap/builders/__init__.py +++ b/giftwrap/builders/__init__.py @@ -74,7 +74,7 @@ class Builder(object): # finish up self._finalize_project_build(project) - def build(self): + def build(self, project): spec = self._spec self._prepare_build() @@ -87,7 +87,7 @@ class Builder(object): for project in spec.projects: self._build_project(project) - self._finalize_build() + self._finalize_build(project) def cleanup(self): self._cleanup_build() diff --git a/giftwrap/builders/docker_builder.py b/giftwrap/builders/docker_builder.py index 8d5b836..06cb640 100644 --- a/giftwrap/builders/docker_builder.py +++ b/giftwrap/builders/docker_builder.py @@ -50,7 +50,7 @@ DEFAULT_SRC_PATH = '/opt/openstack' class DockerBuilder(Builder): def __init__(self, spec): - self.base_image = 'ubuntu:12.04' + self.base_image = 'ubuntu:14.04' self.maintainer = 'maintainer@example.com' self.envvars = {'DEBIAN_FRONTEND': 'noninteractive'} self._commands = [] @@ -101,12 +101,12 @@ class DockerBuilder(Builder): for command in self._commands: print command - def _finalize_build(self): + def _finalize_build(self, project): template_vars = { 'commands': self._commands } print self._render_dockerfile(template_vars) - self._build_image() + self._build_image(project) def _cleanup_build(self): return @@ -128,7 +128,7 @@ class DockerBuilder(Builder): template = template_env.get_template(DEFAULT_TEMPLATE_FILE) return template.render(template_vars) - def _build_image(self): + def _build_image(self, project): template_vars = { 'commands': self._commands } @@ -139,11 +139,12 @@ class DockerBuilder(Builder): with open(dockerfile, "w") as w: w.write(dockerfile_contents) - docker_client = docker.Client(base_url='unix://var/run/docker.sock', - version='1.10', timeout=10) + tag = "openstack-%s:%s" % (project.name, project.version) + docker_client = docker.Client(base_url='unix://var/run/docker.sock', + timeout=10) build_result = docker_client.build(path=tempdir, stream=True, - tag='openstack-9.0:bbc6') + tag=tag) for line in build_result: LOG.info(line.strip()) diff --git a/giftwrap/builders/package_builder.py b/giftwrap/builders/package_builder.py index b25c1c1..4abaec3 100644 --- a/giftwrap/builders/package_builder.py +++ b/giftwrap/builders/package_builder.py @@ -96,7 +96,7 @@ class PackageBuilder(Builder): project.system_dependencies) pkg.build() - def _finalize_build(self): + def _finalize_build(self, project): return def _cleanup_build(self): diff --git a/giftwrap/shell.py b/giftwrap/shell.py index a461fed..e3ed4ae 100644 --- a/giftwrap/shell.py +++ b/giftwrap/shell.py @@ -55,7 +55,7 @@ def build(args): sys.exit() signal.signal(signal.SIGINT, _signal_handler) - builder.build() + builder.build(args) except Exception as e: LOG.exception("Oops something went wrong: %s", e) fail = True From 8fdfc3d02497e7bf8178f0a630c66d7f91833fed Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Sat, 19 Sep 2015 16:15:23 -0500 Subject: [PATCH 03/14] do not build a venv inside docker docker is already isolating dependencies inside the images... there is no need to add the complexity of a venv. --- giftwrap/builders/docker_builder.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/giftwrap/builders/docker_builder.py b/giftwrap/builders/docker_builder.py index 06cb640..292d58b 100644 --- a/giftwrap/builders/docker_builder.py +++ b/giftwrap/builders/docker_builder.py @@ -79,11 +79,10 @@ class DockerBuilder(Builder): self._commands.append(cmd) def _create_virtualenv(self, venv_command, path): - self._execute(venv_command, path) + return def _install_pip_dependencies(self, venv_path, dependencies): - pip_path = self._get_venv_pip_path(venv_path) - self._execute("%s install %s" % (pip_path, dependencies)) + self._execute("pip install %s" % (dependencies)) def _copy_sample_config(self, src_clone_dir, project): src_config = os.path.join(src_clone_dir, 'etc') @@ -93,8 +92,7 @@ class DockerBuilder(Builder): src_config, src_config, dest_config)) def _install_project(self, venv_path, src_clone_dir): - pip_path = self._get_venv_pip_path(venv_path) - self._execute("%s install %s" % (pip_path, src_clone_dir)) + self._execute("pip install %s" % (src_clone_dir)) def _finalize_project_build(self, project): self._commands.append("rm -rf %s" % self._temp_dir) From 94d11ee1b2853a5625c3c52f4b6c925731a62d4f Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Mon, 21 Sep 2015 12:02:32 -0500 Subject: [PATCH 04/14] revert virtualenv changes --- examples/docker-manifest.yml | 13 +++++++++++++ giftwrap/builders/__init__.py | 4 ++-- giftwrap/builders/docker_builder.py | 20 ++++++++++---------- giftwrap/builders/package_builder.py | 2 +- giftwrap/shell.py | 2 +- 5 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 examples/docker-manifest.yml diff --git a/examples/docker-manifest.yml b/examples/docker-manifest.yml new file mode 100644 index 0000000..31fd9f8 --- /dev/null +++ b/examples/docker-manifest.yml @@ -0,0 +1,13 @@ +--- +settings: + package_name_format: 'openstack-{{ project.name }}-{{ settings.version }}' + build_type: package + version: '10.0-bbc1' + base_path: '/openstack' + force_overwrite: true + +projects: + - name: glance + gitref: stable/kilo + - name: heat + gitref: stable/kilo diff --git a/giftwrap/builders/__init__.py b/giftwrap/builders/__init__.py index 1d3d6cd..666507a 100644 --- a/giftwrap/builders/__init__.py +++ b/giftwrap/builders/__init__.py @@ -74,7 +74,7 @@ class Builder(object): # finish up self._finalize_project_build(project) - def build(self, project): + def build(self): spec = self._spec self._prepare_build() @@ -87,7 +87,7 @@ class Builder(object): for project in spec.projects: self._build_project(project) - self._finalize_build(project) + self._finalize_build() def cleanup(self): self._cleanup_build() diff --git a/giftwrap/builders/docker_builder.py b/giftwrap/builders/docker_builder.py index 292d58b..28c3d9d 100644 --- a/giftwrap/builders/docker_builder.py +++ b/giftwrap/builders/docker_builder.py @@ -71,6 +71,7 @@ class DockerBuilder(Builder): self._commands.append("mkdir -p -m %o %s" % (mode, path)) def _prepare_project_build(self, project): + self.image_name = "giftwrap/openstack:%s" % (project.version) return def _clone_project(self, giturl, name, gitref, depth, path): @@ -79,10 +80,11 @@ class DockerBuilder(Builder): self._commands.append(cmd) def _create_virtualenv(self, venv_command, path): - return + self._execute(venv_command, path) def _install_pip_dependencies(self, venv_path, dependencies): - self._execute("pip install %s" % (dependencies)) + pip_path = self._get_venv_pip_path(venv_path) + self._execute("%s install %s" % (pip_path, dependencies)) def _copy_sample_config(self, src_clone_dir, project): src_config = os.path.join(src_clone_dir, 'etc') @@ -92,19 +94,20 @@ class DockerBuilder(Builder): src_config, src_config, dest_config)) def _install_project(self, venv_path, src_clone_dir): - self._execute("pip install %s" % (src_clone_dir)) + pip_path = self._get_venv_pip_path(venv_path) + self._execute("%s install %s" % (pip_path, src_clone_dir)) def _finalize_project_build(self, project): self._commands.append("rm -rf %s" % self._temp_dir) for command in self._commands: print command - def _finalize_build(self, project): + def _finalize_build(self): template_vars = { 'commands': self._commands } print self._render_dockerfile(template_vars) - self._build_image(project) + self._build_image() def _cleanup_build(self): return @@ -126,7 +129,7 @@ class DockerBuilder(Builder): template = template_env.get_template(DEFAULT_TEMPLATE_FILE) return template.render(template_vars) - def _build_image(self, project): + def _build_image(self): template_vars = { 'commands': self._commands } @@ -136,13 +139,10 @@ class DockerBuilder(Builder): dockerfile = os.path.join(tempdir, 'Dockerfile') with open(dockerfile, "w") as w: w.write(dockerfile_contents) - - tag = "openstack-%s:%s" % (project.name, project.version) - docker_client = docker.Client(base_url='unix://var/run/docker.sock', timeout=10) build_result = docker_client.build(path=tempdir, stream=True, - tag=tag) + tag=self.image_name) for line in build_result: LOG.info(line.strip()) diff --git a/giftwrap/builders/package_builder.py b/giftwrap/builders/package_builder.py index 4abaec3..b25c1c1 100644 --- a/giftwrap/builders/package_builder.py +++ b/giftwrap/builders/package_builder.py @@ -96,7 +96,7 @@ class PackageBuilder(Builder): project.system_dependencies) pkg.build() - def _finalize_build(self, project): + def _finalize_build(self): return def _cleanup_build(self): diff --git a/giftwrap/shell.py b/giftwrap/shell.py index e3ed4ae..a461fed 100644 --- a/giftwrap/shell.py +++ b/giftwrap/shell.py @@ -55,7 +55,7 @@ def build(args): sys.exit() signal.signal(signal.SIGINT, _signal_handler) - builder.build(args) + builder.build() except Exception as e: LOG.exception("Oops something went wrong: %s", e) fail = True From d0506e9eb68200f70143e2c6073d278aec94958b Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Mon, 21 Sep 2015 13:24:22 -0400 Subject: [PATCH 05/14] Fix README usage This code is not in pypi as giftwrap. Therefore, we cannot install it as described in pip command. Until this code is in pypi, provide alternate instructions. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5906653..0e7910f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Status Usage ===== - $ pip install giftwrap + $ pip install . + $ python setup.py install $ giftwrap -h Dependencies From 2bd859729ed00181874be6cdc3c5d52c895ea373 Mon Sep 17 00:00:00 2001 From: Jesse Proudman Date: Tue, 29 Sep 2015 11:35:41 -0700 Subject: [PATCH 06/14] Add logo. --- README.md | 4 ++-- giftwrap.png | Bin 0 -> 39816 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 giftwrap.png diff --git a/README.md b/README.md index 0e7910f..25ffac9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -giftwrap -======== +
Giftwrap

+ A tool for creating bespoke system-native OpenStack artifacts. Anyone running OpenStack at scale typically crafts their own software distribution mechanism. There may be many reasons for this, but chief among them seem to be the desire to ship security patches, deliver custom code, lock their releases at a revision of their choosing, or just generally stay closer to trunk. diff --git a/giftwrap.png b/giftwrap.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a13f9aecd083bf80743568ff58e8d0c7a07e24 GIT binary patch literal 39816 zcmaHS1z22LuP6*QxVyVExH}Ygio3hJyA*fVV#T#M#jUs$DDD)8;{NFQ|9kKG&%5u< z_sz^+S;@}IT1j@kB>R(+f+Qjw9vm1L7^1Y4xC$5;1j+k#6%hLUH{@tU{rwNzMMY8+ ztY(tn@O=R5D5d2B1_qD*=L-&&m4gEY21#Z0QPWjZUXItq!H&tu)WO({$e(Z$Q& z)yR|4-i7>MH~Fvoh?}{XI9oZoS~=L0{JF1@v4fkd02$dIM*sc!*E(IT%>Rdzz01GF zdKVD%X9mQ=#0>i1$jm&g{vTw2X8xaKrY8R_m7|-p?O!sPnt;q~&FswVU0vSkSpHXb z?|b{-g#WQfPb0^FG5c#1|0Ns$pOy2fn7KIEy8YP#HG3;pL01011o|iVe|F?wRALTx zj?QK-F7IT59RDWyJM5qMn*YN_kn6vA{2lmD0!3%5cS()@tV@vP-wgf^`zOBE|Hk0& zz`qgv*?L|jD^D|9O>wLD4gE_BZf0hF(EmE~Pf9TdTL))VMwoyL{1N|u`220=Z<=DQ~OK%MU5;#e*^P_{#R`NcU|C>cQCaw z_Yya9H4|iIX5nCD=3!*z`N+b;%gn>e$;}V?2hQJC@MlKM+04k*!TF{koIPkDQQDM>ol>gswdIM$i#ZK6opH@`(`$%Jbo&HCSMzoNKuwOJu_c~Qp z9unUzaZY+jA2lt%@X&^K<9ddIn3Piog&&7VuYEV|V< z#yl4Nr)AqL7NWe4y{L*< zS8c-o>r_OAHJArJ1ajMV^qkOh{(=FV!=vFWhkvvR(d7Haws31yurwcHa@f%%S7Y;e z*|>cYcK~Medv8;Jmhb`qT0}Iiz+1 zBW!qqnX=^ei@1IH1|mGHe?ytu*dj8gk(R0#4AC`-xZF%5GG@3&&Y+y&g+){#YMFZH zW$ve3JQ<3DxJdbnktp#<-&XBrJDRX}V6p#ys1|oSDtBN1tqB1Yi*vFb{==2+?Pf1s@7Q2q5dl zIY1*G=$!PR4A57&t`(lkMJYl#ZeUhI&y5lKY7|hm@ew27Nt^gY#p?pOinxE6t=jgB zoo3>!x1#gaHCGM6n>!Iot<=Z=)cg@JfF#ZVf@@kV6BUDxrLR4&d~Dtxph=_<4!C8W(UOOFmMi)bWzIg+$fgI|6e+2^ro`47YXmj1poe zi$$S#{kjA|kS2_vjDPN>mzME^2={3GUBEI1hVcA6TG}h@LC7l^aSyaL@cEhR#1He;C8J=&ibPH1Wn4Y?-(Q7vQC-<_A*Ab`gB`oZ}CIp zKmATn5k`UHx_%gzhuye^aTQw)CYMz%=t4e2{d)WFb|F*v3}EP6F1gmz(}tL&QeM@6 zr~nV-V?J0z0TQ2kG=8BQ@dq8JFXXX6r4z}5NUIMRPcw^;POTQ2g`xY|hl@845mV)$ zf0(EwCE!H?ti0knAI|MLee_0>@K911gg<17eG}@$nR2B0yLO^|Pi;9v`JcuuNeg(9 z0n1doIySb#73%p$6t>pLLRfOgInf_}({B*zCsfzl5fWnT6k3{ZjEQ((9I{;z`A=W^ zp}+u^q`Jx!6?<7L>1=|fhJsDub;}FvQscAWLp&6D*vvzBrCvc zWboo{pBMkZf!t9}u;Vq)-IcYziYq19{3#H7VqCs}`!+oKynE)F%Vf1LVjkZ{#% zxfe%!MOtQ=^yD|X5;VbC{~gPXcwrWI0m&1|&T$PEVEqVO-`7S_=U)UEUV;)VZoD6W zU6%z-=J~T_3Kfu_x>zS zC7_>V!QWB#v`;B&M0tV+k~Ji7AgZSgVqTnqylq9s0027S}(1e)T>+ApC2DYM-F}_icmyEg3^b2RWO+gd5izCc#!7~PkTWB{ei6at7?I-tQ>Kk&gz_=zx>-} zHdp>4n#-zIr;`5FlMTn25csGmT0Av$uI?4AdDb0!RmPiZ4B1iByG|xR0C&5f#OIb4 z3zaqcB&mp~qF#Ew5qlq^O!dJe;!30S2ywkM6H?o^H9ro@C9LN^Wjs$v%J5~dz|`2m zGlb+KP?@kIZ%Uvlii(o?MW!C@_9J!BiY`SCWhE97e4krYiX<3=fWl@=>_Z^?WQKva zyiieMV-Tj)Fk}rc_``QGU2t5|wl^}r_%KMelcK6Xp3AJ2FtQ;BO;}>_wf;*1B|D+V z6Tn?6#~7|LD%-XRhyj#dkl|j1i;#ghS?RC3D2!zc>Y`_!>_itfHQMyULI^&dk-j$rtmkj;z&;UmZm^zi}7mHn8>%t1eGz)Y3GSRgRj-7e`LP&Y{%j@MxAY_0|)wDDuRB9QtVT&E1}qV zGPR9D0JLfY_GUORzK_DwZhr7ez)M6=4=qW~TR1H^AI>Jda_F=zxG`R1VV&4fjRif7 z(Tlf7VcXoutk0*XM6-MA>;sQU;W(sg1#+0MAlat!MulybMAjf^EQFnTnrHjLihwnUmhd0lVXjlONMtREy+?0*E8(sOZm~E z51~|ADJLuhxC-h1#3;C8B%d;QOxSM^r52lHGivf{*!}$qq^DqkUXMLnyFxLys>8n$ zI2G>UhO|~aoVLo zXsJePv%fJ@#Nc=YEkzk;NQpR}Qk7hb9$(iw!;%66E#I?|>8WbOA6I$>^vus}WM}^! z5Rtaqu^A%bbCj{gfJlAUQ35q)FmjFN@Z?W{N5WVzvg|rq{KF@liq5A=ueSZngZwrX z@rA>}7;0gG6qR0#r~m~u0dmeSJyYd1QRVYK_zxb%?NBr1SQtpz1b)xuCKVBp^to#KJf|YMaJbU(r*mxElqx2!B zKr*V6WHSCRl9vF8?aGU&wc-oPh$`f7Cit}ZfxFfzE{W5J#5QUL5MlU0l_Rc#m?N9j zwUJil=dciip-B6Q)i2H+W%V50*_39RL<8PAB#4mdW#CUuWMI9i?AMOT{HU3kuOGd{ z)+l3Jn0)D~{cd-LXa{SY>UP}E=BW3NRwDxMr}|;dxs%XHddAjBd-k4uPlz2}JpC&g z~X+$iaVY_^s?-$=bmbIn)ap^8ZooR603!^k~Ynpb4e z%WF(B7YH;fqLR?CNaJCx1aD03s)w5_%862~72iYW$OvJ5Sdtz4FG7St9@yS_o;#$4 zH6KI*jfDV_FC=#s=UL@6hnt;EX>Y2w(fKZ!Bs_PFml01DesuH;^g_Vh z|L`SU1zcS7@v5|df8aAoCJ#dJ52u>plv21mKX!k}xqDiK>`qS}TeI-#A^1i+x}$?o zFaZ2Hf$24Ro~i^o3I>W<*7uk1V`7CzG>{E}!t1Su@|w?@V22P6!Ee@1yzG}+J%V=~ zq2q<=NT*Hy#fo6{)g)OC4V1Ei*-r(<>3vZpH&+SNE{wAg3X#Qh3Jw9dR)iw6hMUk* zv}cu?&F20Rk0+A3A9cIkF=nS2M{h`fu9ypF8rsx%JL7k|=ecn&L1CzPiyuX+lJ@+LYQqovwA~Ypd9z_aq^)-0Q9G6bvcUQX2 zT~G7K%Dz78v{g{r2t^18oF$6VA3DHdooEs>MTKEheowD{(G2MDI3$R_Xq<9h<5Dgm z@QDANw>;HsG&gz>DRSeB($DnMuoQ;M$DTY4v;N1J?#V@-E&>H*jNwTPM$%pzZ9Whg zh6P=V!hKwauj!HseIm?EqNoM>BjbC#OEL%C4Fh8}x?HZ;9nmgU8{jNEG!I`}2QXi; zKNtnMT+jIKV&~r%6(K-^S>yw#P-yZl&}o7>AWn_3f87&Hi@=mR^hkBQ$r;+St?KeQ zO*8NCw|Eo?rPS)T$1xChhZ%Cyl!tLg^wy=q=*FNWRhR4DUexo*Sx+W#b~R6ezjriKhY6je zM2;C)=AonABU{s#wO>|=9)S2OXY?Bov>FdC(v#~n{Rr9uML`-w*;9nv#b?+N)-aCD zNYbVgtmASo1M>f7_cn(Vo`!NiH%{FbYCmBzl&f5p`Y1=l^WnZuIG>Dev|} zwdC96!}|8>lH{a#xN@ZIK2)clPIiylAR*EkFZ5wZ0y?=NVJ=e7y_spio;H)-Y|F5O zvT0PBp*3aiKG6Rd^QSa-SU;Y8c~lD*YD3x{t142vMwIrvo%559#YLBL*1Avmfx z>q8=ixxnj;OnP*Pwh?z>-S0#-!&o~jgw(h^3Z2_d%=B&Dxjn)NVL`8fdiQc!`_7E%<%gI)V!+@fI1 zfUQhL{yb^D`qOm)^^fq1sy}9n5tM^1j2GbjDk;1hz#1ik$L9lzwJU2s6lLtJSwo4g zQ_4+w4n7Fvo&9bl7?%+N?ijZL8-z(yv1rq&)iQx4M={`+y6!>jT()KEC?Lk)f zbcuVZi_~GG1a;u_2U8{g{p-HOkp0y{)Sb5JR^uJwMT4w+u|fkwGQ>6|_QG;<-637Z z#xB3?$Z0aSrDaseJs_y|sViPI>V;vXnMULH**x?>&_@QnE4U&4e)`K|BN*dhtq-q3 zmgF`7U?g|+nTZUNHxbEng3jW4_LPmZbT$)yArJHTvUyL;dBc9W zZq?(TP;i$Qzyk{0N1nDxobN7qHEdvU zlCN;QXTn47_-kcp=5fx-!wI6_mcFqg6zTcH5DrANAPz0Bl}mO>oy&_KWD z?CIc3#PP}%&Qbow6_3&5tu$hb&?Kpu5{`GlO;<4Er|KT3<uI=&$Av1T8)mX z%t8BLS1@~MQOOJjdNedvI~{3CRl% zExvsV;A)fW%t};XTY@#?w6>{##TM0b;tl2H+oRFKW`LKVcmQfn3nAuqzLg+GV}%hS zToJox(l0#Gea}*iC^02sz&r?EfMd^a772Ctg6Yb)=c53LcXzuB8^}c28VJU(ayYM; zi5);}>8lJCwwYiO5cydefH?9@~)+O{*W98gw7f_ENl)Nu#iu9LGZFYfuOw z#Z$*wWOcDkA`K|dHaiYwRzw!dmko-0w zv%=BVtdgi;mLROk`8my*9ANF#OgKfSW9N$O%9LN#siR$6& z_h0m{niPl+8%uo9NuozMu-Pn$ReE^)ik>8{5_3@x9%rvJoqlJuCptx!X<_G_HLaTm zv%6(1Y@L9Z9B_H(jbNfF*V27LhsO*LV!wF517{8?vmJgNp*k*9*OisT8Dw>4x4LER zT~3T+)H?{~PT1+@!|;-2SPa-jZBl93TPYaj?{?aI#>cmRG}cb4=6G>^@WkuyD!W}h=^6xnH^x3%+T67PQ>%JAhe3!AoRR3bGKDxJ$o>t z60>V*bFxRP>z3ch1(l8}L`kSoFL==pb`3F<4?hUw{Jxw0<(cfx zFf~AG8({Z(B3$m5^67PUEdq;Y-;Co=}{Uls4vM55n}_Bu1V#~>hGzn*%09) zUm6uD1G7^Zjme|dTqb|PN>BC^7ZDKNB zo1Xb~63lQ=5I4-M{%oi*jMJ+2j-PB+be0B#tyFt21>VxP?dR@~OnP?$o$&OkcdU*w zqm_^e6lc*Dc!c2cMRh>jl)>AFNI67kspVu)MC5Wnp#;&**b3UP44778ZXmP zumUVgcu5-IalMhM9D#lFj4AKjTm|2LWFnV)41!7CQLYQq!sA*P0a+J0Yjs;m^=IVt zY#7$A8^5RF4)<`J)7NQJ{;19k*3vFh2WfFx4S)B-b)%P!a#Z6nKw0|KW#QgySBG{M zO=LOb3W*M<3#BNr7Xu{G#8%w)?g5D%%3hn+cCor*WjyGx_D+w1Ws`(*&R&&9Wxyoj zy`r4;$`aXsuJ|j!2LouugX1>dw~U06T3fNy^%0KT;)T*naJL9GIVL8gR1#BXbxGZ# z1BoFvbh5FP$LCY{reb2n46@5em?0kprgaChyHJz36 znDiMo1EkD+t&BE&xYsN@)>v+Df&!0LthCPLVY170wb5i|i9Pi1)(As@m1~WELP|v% zjYL_Bb-hu0cac)404;1`-*3TY7Z!?@e2P(ZM+OrKH$|2tJFlWZ6{;|RKL24&u1OAp z^O##vHY(GzjU^x#!$Si)ixEdiJCX5dpYiKlvT9k!F^^rzux_J05HCN9!FcpWL|cgM zxyJ00xM8`isZeB+08uDs1G4%;u@{3Ppr;n#KCQTnpGvurWi3MLh!aG8*p=u^UVPtw zpc_DX+>nw?&G2fMdECxKk3onh2v>^%nZVwC*1TC8YuuN0k|cgNXk!Cq-hiIuvPG#E zck>(OtBcMWdV%$yvU6LYFdnzmL4QT-w=)b&)^Gb07-yZtY7HPjiik8Z_pJKJN_6&= zK}qJ&pr5RS(9YF^hDm&!UZNbu%$O&SEN=5=@ZHm5O}X!+?2B$y|BErZ=h5Y;dH3fY z7gO`DmWPE0K4<5gGNsb)dCNjCFclt$}GY(t961CnkN+y?UtA+M)dvMdjmnoA(n+s@+dc<%(1sXGw-w zF%3)2;Lih$2=EhKIKhR$Xmw+_)Rt1(s;D7C@AVeJE?u2B^b^>^*P1ZIGgBbNH|s~g zb1^z;U)y%y6DG@!bD45{tXaA~s9GrYwY-erXWFf~7@1NSnXY#HCGrwPl8FobG_KeL#Gu2OG4GRbbRTh=q@A&UhOQ@BikUPT+JID& z2*s^8AA?EFM#Gq>9ZTb&*CNd}rr#`k(RDctu5RPMPpI7l0g>dFS)rp*cX0%{(uXT7 z#}pymH`wDt;~7)rC+9?5XzZj$%(S6QoiBsISclPLiK_S#xzPomHY0nK6#aOsijTJ; z17S#qv-A3z+fCjIQRdwFPE;`e)P|i=g096;sd0YYXLTcSnT>fQXeY?B!5XYWzI+** zL7kR?W+9>(dftHdndNu!A7$}4?L7EVIYgPWYM{ewba2gw9{?rtySpGSem!%pG39}d zz%~T>g;RaZ2X((Gag~9(?aS@{iIXY6yD>D?Yu547$-}5cHoeVJeju|KY+eaDgyTbO zs4@|&;s*($#7|ZHOrv8`PCe=KvSGqmbOpIP~xuN7O3{(!bF`EqhE#)BLl(GTF9-$wX0ns^tjBkF8_#+4D! ztz5xzR{p|p<`zegQ9DeW!#_0NM7qR}j%&YdYyU|ZI0v@_`x5x{a%w)ky29?b?$h>d zXb(6$Ga6{2yockG1N9s>7;Y3$19)l!Ur;(c9hMk@!G-T$@f_>R@$_gQ3&g1kyM4|m zt|FevFKo(#GC9C!2FRLka3r5aTtHPIl3!C!J2s;8*1@g`tOE6*tLH~djlF2723dL4 zyZzmMNKn7*tKm-4KLLGepbf9M3005<_A0b*;Wi>BAT6jm4q$P9t@ST8eMZKS^(wIX zk>oaPNSGhT(RCi30LGOy*4uCIpzGfY4;wYqrQ2qDIx4z2tYh9Agof@3;0UFBRwe+aEL10Q}7*& zla0286$Lmp4efsSxei}-x3~%z+szE_EU!eO9UGD}N!xD3liNruq{f^Ljxb4a5tNK;{_Yxa-;h$F+T-5=j z0l%

2gASjL2j<^lJS?K|*!i1fU&L8e|3Wp=X}oB$dH+>|keZ(~iFZDW!@r5KjC6 z2iEiml9=#%wyDDJ3lTb66Ip(qEu1@htHJIDoNirS2NMTfAriV%KUjDbRt}?%7v;^` z`I(jpGQX%kwQVg-hU;MQCx5YBys77!N4$L!=Rljwfqp$^?;SKP#>PV!{D5lCxeBp= z>}y{Tm0)MskYKLf^d1bY0Dg>`^D9pUb=E!?p1zO^jeJRe zx~9R#A@f5K5}YAR5Himt>Ls$*L#s(SFU6o%zsv6??!yi;MetdyO+{8cXoOtuj?tLZ z0ZwNnmy}@nl)Kj1bHfhQw8Pl5!G_=y{+nbDY ze5pU2{mC1P4Yc4J#-ZO-2RP$On|ZaF&P)Ld);qy=F}TEb@+d;e%+Lj0E1S3rOvb0` z>g){80pZBZ!}@AtEZ4jqePVF;Z{wk5*1BcxeKFl1FD)_9O(NsdLym%+^buOV41PKa zVJS;F^#Q;vWzuvoDnD2*vP^XiGAh&r@2y)72_)MFs`H!TEgKM5$D2+vYSszsIA|R( zr*D0v=-68C1|gW_?k!Gj%klmEao-aw{41A(5aBY7PXNxt30lx-V|*sE_tOi)V7ZL} z*~|vJD9+y8`}53AuQBd}Xv)T*HucU|c>oDKj7OoID0uPb(wZn)J8;vZDJ@ptBox72 z>)}SI$!UEyicslCJN#v`H}O}@!B(}wFI*UX!{Rwl6pV+w2es-<h=w5B3n;Te{z1k7p0JV{j?E#3PVdK%6cHiOhsTQ%fm-F zMzoJUP?@y_%{-nxvq~ShGgG=ff$#EzZj$4ezYxZM_bB&RJaPe(jo_Khp9Vp}ah zG`0UB=aY-vTox^R2FB^9yg=$_Sm`G!rDIf0T>3r=_})M4vNs0g?p@S_@1r7J$bI+9HC)l{^hx_}Z;JL83i__oHTJO-_^E=6K zhz}eHCkvq=AS??LzSJ~vp5Qr7=hDsF1W1Yq$n{XM9C8+&xp&?YObI;3 z6C5H?Iq?q>S#i(Goj7bt@yAZt&**{1TFS=~vwkkGcME_>B24j-fsqw((Ffk5Tq2ZX3+K_id{l)b&!uQ|Z$Bv3PRJc8X_NXD%v*|=h8RtTf4MsfafEl{IPlrXZcMP2l zcKk-n=$6N>#NKUzG=j*O&qe$CUX%T((yLW)Ix|oZP9kH}DtaXS=#cNYYrV zyFC_MyHjNLdyD<*j=@qn8z{FTeW4t_h^SrYK07UWJK}rai%W>LAv@TmSXr<`EB3y@QV7f zRk{ioG8U2}JBtiK0U*cCt*JR{YHOeA_IY!`b7987>_iWUU4P3N>!$W~In`u#Ll&O% zMIK`2sYc)*3``{HLy4?<@;Ai55KwuU;BZ;pRQ2D*EOi+A-~CAi74Dy>)|^ zzF&H!X+~Q769CJyo}|%-c4e2G4N_w$uRUD7;8LIH6fpSObQTPV^C@@$4Z9dHjQY?( z^;v%u1{wf44bh`XY?*#rJqmrdS@7kw{)v&}D$wh7Iydz!dr2mDZJPCaxxB)H-0(@6 z?6juqxNQz*k&|3|QL)?Nz>u2R{&lFBi_}n191u1aQbnxJ-8%f+rrTLN;*`D(SUS9; zgiJ-~2gXWCd5!C_>W&A87u*XVqy06cbG&l$j5wRTVqt|t4Zsva)9)RZ?Ux*qgF zV0ka}z*Zvi(`A-AGpmu21{BS5M!wWlQAfxSDPJF9;d?2*St$TLAParqT6Km9CKett zN3hF1`gj*g0PpAp0{oYw9WNGv={fzR|H>t?)Kbp}m3oayURk^H@07^7wpqVfXl@I- zZi!a*(kg(BaTrfZ3(~UBP~*FU3d_PMua{Y<-jLV+#OK2aO>nXbz$lgQW~4eOvPsDv zJg;aF$(wpt@#_iAhpXQo|75#TxvCy$8MKgv4RbSQe5NUToLN3No)$*oFEE&ljHg+X zsID8b14ct5xHc=K@-8hY=i6M#49~se<~!`Mw6iF}N)?+$_ZJCg!ZlHhM%7s7FHz6F z7SSOrGhDkI;HSshJF9>*8JSJ<>br4=z%i!8AT=Sv!5=g&3t-_I48y>~Ir^AOyQ6Dh4>hb40x`>l^-_|E? zSI(rT`VryF1O{Y;;lG=D9rrHkS&gHUGS{58CT0v%{Y)zo4=d86UNju@jc9BRw@i1r z(H*>TD-(XYBboDMCCX64>=3LokIYnmPRB&yd)* z=D2A>dO}3_p*5A-+THUC8+irVOa@fP=r*)|flt1RL6X0dNit)LTwG?!41HY8#Yxg{XA7=JfF@MTU;mKF*Oa5w(Y)A_t!+G(LZNxlLnj3@)y$F~^?aHc zE*2tRA|-eN3y0Vy!aZAx@FDst|3snw;VI!Hy;e^Upe2Y-tg68k7i{2+*wANzd z^E9-prc;U5lJtOvB0SXVix`-XeEBa9)kH7*S_;Qf7MLYsXl#V zSa?1L!*k5Sryf`z(=SrY#=iZWz&NCiar1h^huH`Vc;Y94h0U3ty8*Ua{Rj@*r2)#F z^6>P?3U64m?&P=#+XxNCPKuA=i5#I?fvYX98!Pe@wtEvI`qaQgqEUFhqs-FW>9=iXm*U9glP35qmGYXpTnKYS0+0#f0t&n=IvUt$WPuS982yh% z=iXi2s@2QIrX_TU6%io5nTAABQtkl@Ijf76g*iLZQ<-rxKRW-9{OZyILuFJYO#SSkHbK>fSrb!oYT^@3iz zKUGBKQY3&Haduc7FEBgSL|LdMp~tH077#Fcxc--Rt>bcIw9=E{YcEB>rVo0VwMm-+ zqcA9U7p=$;g;RWeQ1$PxG1{kYIqEP}+~|HISbfc{z#W?z^^QvLcTs~;a>pd)Rg3i@ z1g6pg;emTTLhE=S&n91%jK0Isx{y_8{mQjV4HSy;W~hEm=7-d^m8(ZT*NG4@=7V?- zMol+2sh#A8AG1vX9=YFtb~gc71p_-_$YM;>Xef;o8^o0E^xkt65BMg^%KDAB1Pn|b zeQS&PckgW{B!@&x$X%NHUAu+SdSzFewHMEErpAu9odvNA#pf2cL>AJwnVjIo-iOQR zQb+Dxi7H7DDnIy8d*HudA{&3M!$?-bC|#pfN^D=sRb$BWA5KG~{-NJpVN8w{4b!SW zUB=q1nvaDzUqoOoJMGzL+#prAJISjvf@9VVY*u zYD%CeSJ|^Gb3&BCGN~rTX)MZ01ol=phiK~|#YU+z5QJ7OHhq4168JbJin+PCqMffy zN1~p5Z-gtfRBznduBoRAXEo8_ACo&b_Iu%!ZfMhXuY4ch1n#ASCF;u@5lYDf_@hAC2_>))wzK{Av+iU&1)froS*V*jgSRkYGjKa-hX>1FFb@2MF|vDxU+gygNrNCg}aeg(mp z+{_(xo^@nBz=iiiPamknqhBCPY%W|QvB`N&o}W1}Mho?1AT-p=21BEXBz>u=*rx-` zK1O5OzfG^tQQ^g^OokfVmgljTWArmjnA5Lbrd!HW^HSG2lHXQ31)7J`mQ`*T+7XUF zmS9>FW7#MKc^l>J0u}{4;gdQdQ6I|&ADTm(L}(-r{Y2L8ls`8p)R0RZQ>rc}f84(I zQ^tz{f{fv+ka-0&B0CA2biR^SAJ&|68Chse$k}|#a`sqZ%->M{>Z-?v?wgUl*TcCk zBlyWZYyI2rE55E+Iu-b-AzQ_%4;p1@#q8&x?XoQR4(3mnEo{G@%N><;pG;3z@25;& zL>q(ZIa0@n)`!M7Pc>_W%sXP+yp)WmXAaocSBN6~sOS9nW(3iQg?rkOXieX`O8kvN zaSufzAGh$pqcmoG!HvSZC#>^5gDJfyja?I*|GXd7jTiFmTi8c?JXVi#a(o~OM5-WI zX6-LO#hIryEBEaF<)${v8)fSLZD<`@uX}z=O_@(tK{Y7Z8Dp6ESh4mlDglp#D^$Yp zKBz@{ii8B5MGjff=IcK{c0U|N5lHJ~$7Qa9<5WmZl25lfZ@rKt2`b0GcVuJt#y-pg z9SFWkjVCuhHKMLA)olmUn_xLRZB1lPk(Y?+YCv1!#*2aNFYzX-Ycr2zCB{x$7Uz)5;mK!mF3GsFX_|nY+P~-)rK5qmd01umDvbN#ur=pT4j04o>To7M`kY zB9RQTo3=cNJ$NA2Rho3SH0TTZZZqk6UVd0#GW`HB0-?I?}HjyFno+AtreRq65RM@QKamJo2h$+wUcfu+N6-R6YiCyUH+ z{$UpViULs*4b;sB2b5HJZPLy%-QJV}9~|@~UIMda*@_67RF7xRUXd>nuVxOr*n8fe zRh#*OH}{Na-#fYS&)d(~6vMMsGHqnRS$y#V{AKNIPnj5G;|aH3wv}g2@%DWwNJI=- zg&J4177d0MtMKE}DmvTSoa}vyInuGT9XihViRn;^kUV6wty*wzfpZE7{M(kNrmW5Q zpfi_Mh`rtsVngV+<+#+V^(Y#NX|?iL(q z&PC1SH;VJp9JEUbH?snrA|R(J+Z)7SYc1*uNJofMPFlV78tBFvHQ^2uy?8>dDkrw2lym^W@orDbr|0u zFRexAw0#_~0og199c)WX45x_4ywPbJ*CARW zigIH8?ma`mgw0C5J6jw9CT-=a8*O;r&FfU+Z8y`3ICp044UO!lEJ^B;_5gRD(Au~q zQ~Cg7tSUyGLdaxd@y+l#<#S?iFg2l$4iZ;Fv;nsU=p)lQ_6OBB5#hxa);UXXY#0w^ zkP9^qL>wX{?o&0kQeFWv$K~=E+n#LvD1e8h@)dF?L)WwdV8R5h_^JR zqy*$^Me$cPA?0u}Y`Fgek3ew0a!b~HCAOlsQxLJ;kB#uHO=TC{f25Z8Z%r%bKY*CcxJu|Ve1ClGMEJ8OfFs!f+tj!jGorO*sGq2%wk2`LIw#F`xe~~>-O(; z&3I|YPZl0XND?*^op4~^WP^oI$0T(LtRXZ{r6x-oo<9ggMVeuL94)#QKvHIxIdo7v zA6l4CyP4c<5Y#`ZLjV(kUJ5S&GJd4#p!+Lz?aW}fogTQv2UcZG=s!ThnjbV?>dYcB z^RDLOk(uU8(Cj5{zjq0GA^ZWvA6ZvSuiN4&sI+&JS^o`zEbBBV0u%!M7=hNJ@axFj z2n&J7R}Izh|8WZz)Oqq7DUv0pf}0!DN3-n&bI#t_{iFE){`T(STx@bZK4LnBRtknU z!n8P{r+XRNfKae0Q0*EuZ&z)5;63fQ#{2U_v42BJq5un++6v>)8IGNQ=t7NY7D0+I8!~ZSAaW zA-oWlL(3f`cd)|;2~nK43(md{nl4LL_&D5dw=a!9KEJVX`TSal_Mc^jv4gxjJRd+8 zJVmjkg(;pX&yG&3hh@Bq%zx)Lt(bp#^YZ!sa3p-tND&~u?bZvnG_JVd=Z0yX4eQck zJZ<~w*-6PkR#Ikt%1c`*v4D*H_G80XsO@_ZU(J*~|6ay(^wz;bp_L3te&3Ho2oi%Y zbZ!c(28-omn1vGF@Xc3_eLhjMhp*Sx3X@sigYR(>v^|GUyZJ&{g@mr3!#*hf80tOY zrtZ;JwxVX;DX`jJ1Pu|*A~|(nf%D<<`pb^x3;&!`gehYB9ReSK!K41GnE!Oek~J5gzj{0mwHjlt*H_uld)5fW_`5p2Q5fx-T;XvAR)99}ds;sT||Y=kiNa zCSW!#U-)=sP5mWIYU?0`3`$N2A%2S$BTx1*0B5HgfZWb_ecGvn5M7ztcA_sIzgxCmZWUlvJ2kEBBPRo}Bk`!nKDnCQP z>`64!@2et&)v~doRHC`|!94`ylXI4te*aJ$r2|B!DB5Qtp+z?;Jg`6uUqK->w3DM= zk8^V0fI&krgU^ODeVkH#FCn;;-r4PNRRw>T_-MLm{yR`Otas}U)tQd9+|SJEiF@f) zBHV!u2(y;n zlKi!XK^M_pkq!^}ciT$tu(Vx_=2G21(Bl3XZmLmPZ2UCGYw{~tu_@cnL5mr$x$QSs z%G>wI3^nONSGJ`@OSwVyrz-E)zvRZ^0CV9-dTUIp z(mx3@pz^{g@1Ht8%4Ky*A)pXY2pmSBX}9@4CbBo6P&<6i)fa5G2A+BL^zYXfyQycH z{FAOEmd?iQbyv!l-2@(Erd|(Cr3b4bD3r+Zg%~|IV*{snf){3y`asMI{qt^+cWian zXAYI-!hk&bzuQ(V^q!tMhlZ1FQZ#05M(S5=GVJM%p=zbDgJfmDAvf5OKmawyZ#v_B z+*EZ{cEjCskVlu@fs|}8zk0zm(j9rMU~U;s43`@MTuvrOnx3@ucSA8DeIFA97{&}T zzDGz<-{@{5{k%@GA6WS^eRK+*pPoLf85vxXJItbipGJ__Nh!^uMw&ZpWT*+_`sAon zwG{#ifqsub{X%3!MKH$8&?Vfx7{RL-cGG;g!jYRvpU(foYXJizGomn zAVs&kIiW|}gso&l#xALIwIQKrt~JQ~-$edjY;fX1nbL!D^}{m)koyFzA6n5s$bd=u z@p&NveZucKfXI!jl8FVGaNFC|#`Tb$?JaM;kP@>FN#@d;KlSI^C&nzJE+_;P0tx{j zz@_;dl!5K;^mtv+ks^*4`eoR0+*$Rv>dL`qSGi}>Z2@V|&O?89m7aEc*~Z;iEZ%}< zri*B2e`%eUtSTI8b3BihwuvbKbW|iujkO0DXsR)GAFMwdu zoCyi^GS8VUy3wt^CV>DRRDD2ZV?TORODS)my}Mw_h|1G?Uvv7~r~590kMYU270_uV zhvmph!+izAxuto_ZsV{YRZJnE5XgT7!WKh{Q+^O1PlKV9nb^VkWT%y7SUVPOrz1N9 zy`5GA6Ggg1{xrM$Zg-LEwYAVx@s~WU-2w*>o#m8B$Sf0)wiN;BUX){C-c2O`MG9W0 z{pC_QTZ=8N_cYz~oNhOl#*-_w$Y7hd7c*A|;D8QWI+A>ygQ6)b0`cKd%n6B_p~-U5P+mXP?b77_YMrtV4^c1LGJnAEi*%6ygK*pEJF;fzSY6jiRuAIH=&INJM*g)iHPN9OI?Bs-qB42pk;W{^in_TbtrwXG+!w!WbbC37DYfFA%IuQGCO#P{WFt9x0MZf8^P!su~~}8ZfXAF z1wM@)eCLO20i>#2`yx6zPbRZZgLC>+$yf+rV%GHAH9emZ6DkP$A-CiQLjttr$Rs>pb+SZfDljg*!8*R)%d@lw1@Ge za^&K=VetzxMp-1GIXu&K5*N(*m@ivU&(nSqx@Nuv8h5#lJE1H>l1-ZQU-hFCjg9tN zfS7W;9Xpb!aJ1_YfAr0b#S7k#b~)$IL8}HiZ8LT-59K_qiol#ffZ{N?B{yKt_IEN# z)CaX^9l|5~45j|{blCC-D=>JGs|H+guj6a~IvnQZ?{c3HC_z#q<~QXZYB$M`Z9YdE zN&WS78!}VLwtfu6-XBP3?a92IVlr(8KQIVzFFvbQI(`7Bu5KKKpwAmzYyUiw52!Ai z@zWh6JXTlL6aor?qXGfzdD|UxU){!Zel`Im_a?^UMKA(9VHms98a!ZsEABm$g0C%a+BdJ z+E}x4j)aeJkYW|`(tRu|n5j-}1nhU4ix=q~%`7Awp<3GdNGfnI6CaclTpQryrYjdL z$IK#u5!~Id1e)ZD*qJm6HV8q{eNoQ4I~B=C>;Zu8-9U=7ePl^0^K_sh1gnTNm?SL- z&PkTRjtrx7F!Tqs>19JBf|}jFr$MH)l4S%*J9cjKs2%hG9&pmQuy{3s&nw*eKRao= zcNvN=5&nWnraUws*Y50nUHw)FCz%ld%VKkuaSa|vDce_{3WC#^>T2@tu@baU0m$0XO!0bKEnu}f?gkw z*R#)HGax0w3goT_yG||SWtuc@8M`ns zv_X<6^77hxq1TlF(z5j*e-~1~U!Rxu&TK(Gvk3O}LPL)_Tn}LU$ua%3^QdPNx=ju4 zJ+g!#Eo=|kT>l_U{6!IA$aUo-6_9BPNRr z$P)y>FIq;((3#eh4T^Q$x66>$N0B+frQ)TNV5zxd-dzbG2cB8;&%vi(v<4INrO_tl zRP10aX@~7(4>0B_5g(If@G%cXVl-$%P2!+?+I3$+lkgX^z=-k8KeQZchqnhM32ORQ zg()A|u=XN@%sp;Q=G(5F7@WRp`@PJspWV07G@KzRLXo1jVFeV8Hcj!YM$oYdbG#Fg0DIw)gDk{@#AO6EgW3p+-J7L6o6 zgfR(2odS{Bevq~8SdWLq+hfxGmZjo5=e8XCtGBPIe8X*fMp5{W2vmI8wpzKZ?6Q2M z59@E!hE;kB-gMbr>WcHsn`2W)ylP1ykbek3&GR(eHo-k(-gbFJ;y~D}v1988#HuQb z3MRnPU+q?bge+9F47@R*fHlWz+sXMoE55jw3Ar`uIv%fq$dXeggW-?IDmM0pxzV9m_+ZzbTqQAI>GHQ9Bndb-7@t&wXg@Z*&9bx)Qr)zZ z_OPWjX$z9V7BRi9j2f;A(;yB$S-)-2nHSxLiTNj846&2JsWPcUfCQ_K=FO~l@@dF~ z1vgtyX6_Wx16q)gWv7t(&%Ctw=GUjROuMV;ZanvQ;F-e8lhpGcn*>@Eqzp5Q-&?;h zx>hoF zLe4<_r=yW6m~{--3rk&F{7E6kjHgqPT?q-)|HY+sel+@GpF=I(2Y2!C$51A!i<$6? zYH7$yv>m&dm{;rx&tQfG{&{^^?CwSG_Grn&;g0*pgQG>EeXg?kp!`>1rX{B zp7i(Xrgb46u;a4I{3bE*ysip{(QH4!+TMDW4LD&Q*2nCD=aKatt#*o=_qqT~jMYkV$j8UN>%&aBk4wP_U0nVtj6q(hK!-o+=?9gy^a zG;Q2fR*X~=q1sajctPNV6Bc~%(hF-hVt$+JHcJ%=-B1$wq3oc|S^Ekg$j{1`t~A$As6#+3r)M+6Qk8d zp!%y2$N~anC9PPxW<3Onx!8@{Jq55-NGnJdkUqz(Wq&I=$l1xRGi*We1m^sG7`(Ui zY1G3BVfdR2f7|Pb>6{fUrZZ-fh^2#udqzMjszjMSv|yvd84x5miyHkBv(?B2T2oAO zyDwO*+?NtSmBGurvZnq__{E*ag5h&v^1SVq8 z`swKU49k-MLalswsHARSQOgBz-}ro1nBP7_Il4}e8yun-|R-F=dasVeWs;q>6&Ml=9}e4NkD1S5l=Tz zgp8T!yr%#H>yE5ha{~A377Rri_}cZLEyIc;&oK7eif7e6p*J1a`*A7661)Ouz>A>J z-dK_ZP4G@&GE*gt{r6vXlyGpL+{y)zssQ$LysxB zf@$W+-c?O2JXdHsOe8M3IMOzAYdz+IRB1>lr_~D)>xawviD>8MS-rpiL0!WsgeK6N1tRQWTFO$!4ris96DDHag8t<_my$Ke>svd&7!^TvO zg|e7oQG2dP+xxfP{bl4gId&#>0A|U`Cbu1@VkwfsQB_t5xI>^gVC?{xy~C4ohcx*b z;vl#+zDKVM>NIQvh&?jg&({%`E$zu3?Irnxo8c{p)j;llchi)e>5KEZ6BgeV=uQIn ziRqZIAauDDuBZ`^0O6wrQ|3$E+f?5(i$F!q+W*9j_&=rv<|E)pvtyv#3d@-#xj*&h z?nTV=ktZd9*kcdC3SwDJZYQuj$1O+5$Fquwp>4+o1f|1^CPP*b><~;${aYPc_>NQp zv1K6-A$aZQsT_wyPJXxytk|$CECs5ti6nef~+Mgo>(qr-ub$9J!$ z8|)-(!U*%Yd(1j%*fj9q2Of8L?@o#O?gar@K5oE5^LyNj3fto0;XZolSJgQ>5lC79 z`S2gVZpKveQv@hwJ_zCHyk|W}4NaQ)(Yq|T=S^DddR|M{FJvl#g6p;&i^fx$%7n)$ zbvp|a7V0eEFD(26VnDVa)uYoYG0V{o^3c@%c~Hpz&zObMeE}-Hq`JV%g(ixG-ddVe zJF2omKq2570qB4K&ik(A56M08AA^4?b%0izZ&nK z+aV->*>x+Jm!zhZVBJ5ZdtFN~q!-pb&3R<4MhM9>8;*kXwcRnxmG2cx>%W8hxdIDL zv74BnT%ljlk$(uJgk#mM!e2^eY)GZ%I{%=|9>t_X6KYolHW}<3fjyM0>bMB zX1G34S^GM613tJne9M%(-l=p_{*|wd`T#8VYA+W29_>RILySsTaIxz7CF8P}oMzpz{>AMVkjV_}^uY3c{||4& z&vLEGnujih0`mtjYvD#HU2>Lh$5GRLN(7L-&)(aDyx_}8-F(y_mqP<3%SUKm6YC%= zhzH+dcS9l77rXbBYbQcbNPJ8^d*{cvR;ni5 zFEiT$5v@f0bMRmp7&y>=I2|t*+>g7@iU*!B9D)m}crku&9D`mH$$nSVA?21TvcwZJ9ekkw`snM0GYSk5ur*ai$LWK>&|4{e+&2sq_w2=MjZ-) z%pi~w0fb21zt8vs7}LL|p8?JcFnQGrCU5d7Zr|||tRR%|$>l=gaIlp53Rz(`hs^LV zoK&g2uc3gO<_%2ObxZPVC$HK%+DX05S69FXSgwSMjZ}zEs`M&j*DYfH+cR-sup?aBSYS`>0zYZwUDnPtC6asmRzz26OJcQZgX-~=!V&=G3 zDw+5g0Z_ec%|uv5rqes^>LBX}lkb|o;3ik~92OQG9k9~M*CK9S4l5BwSaY!6kC(Ez zf@%rPD#zx;W4*ZV*cKipce2u!MrunPnL&Uy7G%PtxXU44?ptQ4RK4_91X3n|ur2i= zXi$ELhq%SHCZ)fko569BaL}>mEf#$Dt@KzyZ0#5{`Ih(r(%B;7J@HY{#?HsW{dp_c z#)Qv5TGNWINn9&ZqFtAD5O%hBS@?xNK#SwkBLebqr~Rd?oG#_Ncpu<4;YE-FRbWE4a{t`LZU0G2eqF{0IX`wl~Op!*Q^!KYE%5XjGl zC8gA@1tj9K#oyj}^F?V%(b>5d&}O7tjR8Tt)7TT6F>FMIsmkKVLm+VrQlxQ`$++B@ z4i~dX2kPIx2p~sWNBD=>Cu@qQ%{KcssLmvf09@dXq9b(z2tfPrnFpSQF8&_R%+^r^ zd^(0ouF8kr{x=qa)GPf-5P*;F3tC&?FHUG5yK-kCWR`285sMU$c;{U_<<>VUoz$lQ zcWzpDVOeuhyjvN#D;QxY8RhSuytFFi{?gq#a`si2w6Rc9H-y8v!7{}dXC3B7sZM`z zM;5PG6i1RO$c9+g)p>YIJPE~8uW@n3%lcz7J^J85+QRqG|Tt2E=&!ly% z7JJDG2R&Jw5hrtICvC^M6|TC*MBdxd(w^%2t^TGTf$AI9&gO!D$zzCm{h zkeidOaAs$A$VuALKvc>H``**_4TH}3;+>!syQHflqX=LEutII@&|A+j|FFuGbxoPC zpdEXM&|_M+IY)?_bQjJg?l0>^g7Seyes9@*^BPhu?YZzQFH&0 zhBXcHa2Z9gcsl4_nxT;3+)=UvPkpFL3IPWQkXumIk~Ng7)^??GP~}Jw;=4E-hseBB z@hneuZJowT1VQ|raC>Omkw#PhZO5$(H&-sLZ-fAHtgBbnW?tn8<})ury#cQ3b`Hai z8#T=hW$dH(Q$I4*Vl)IMEL(fLVad;U6NtODhqUHfScZwJd>x)?#DBP(1!zvQ%-}0~hL3#5Yu`@mL1FYa$;GjfxmYu3 zJVV0GlLjbM2IVu@6)8Z%f!ISl^m~d}_->UbgJDE`()pZdA5o%{`+h0lv z3brZLvYsr~{r~5rL1exfh}4XWVwNSwlF6@4TD@mz%rf0BAmDwMlwvh)uAA^6VT;Sv zPJ`qVwwJO3*1JhH6`2ppSch>^Qtf1@GHGxiww|0wV2nqO9e6AGzOZTSidRluQwU@N z0TB=zp?`J3nfIg$?y=K7E$k6y;1yOoHz_yHp43GDg>=LJihd=*S&HA_d=j)IMF~nq zzica5$hhmpeiFS&bv8Bv)!+I@8H@?%Clh#XEBj32RqWgHrN-qKoZWcK`S&-kSonH6 zg%28$Kd6I2wwZ|71I*VeSm)tn9T37K0VLyxKB|~DJ9fNjbW{TEg*)n%s3+4RfKX5S zM;d=X5b>W?THdI*r}ZSog0wfi%EJ5i{Qvg81HO*p&VOci-&3(<*_I8&RW8AR6LJYj zNF%`ArBi~z4v>W8xP%%=unpwyK1p=BquW5}kkA4Ikc@#p?LrbT%?1ND#R6l?mfVD! zEZb_Y?9TkZzkQM{$xqTNyRYdtAiphBe)HYknfcX8KO&aWHa_?NP1K~RBy65(1dC_& z*M4+&7#a%Lh!!1g+TkB|UmNN->O}7s(QpF|dIKf0J%zu>iV`FE$Uu3rcO8z@Y57D! z4`2nLL{Z<$ULO)~d0&xn7}WBgP0P6vcws!=w&nUc2+7Qe5y&nAvOT&948;1B=d)DI zMKJJU9L4?|nk3A=Co~6V`{)kD(@nmuRHdf$ojZ};kjbx3zELib3IfV$KMXTUivCEX z@WMQ-oF`3bd~?&HQx}8r%$&$Pl@8}q87)doBU+gal5Kkgefa6|1h&(h4DIPPHdsrw zwT>aJE8x7Gv}4;~)$+Xr5Wt|WLam~+k<6A;PhcdVVzvJ+D%%s1j2-#MbrYphmx0M3 zHOGL71c^|gUUcN`^#=`@sn-qCf_`OG26>AGx&?M5)uu|Cj;KQ4W^c~Mk+2M$i^47Y!v-&!7 z3_z@72hM}T`V>e<2FTHMnN;ppV7jfXpADXI|J|T0x&!vAn|s3g?!N_z421}eyk&?H z&-A*SJBM0tOvikKI?}c2*AbQsemQqKb|57aE_-YuHk~{vC3BNr)wx`U603S$;zLFt zdkECdg@D6YiGe0#3^7?V;J=k0U3+qCCOTe!^LNQ)+Lz4^2c$oS=9^t@515T_c!_}g zOiY-UWSyj~QV1*WSeI?NVeXJYWRk3%vy+Gs0e`UD^OS6Wfen2o;=O?RR~kc{nH*#5 zp9Z80L1`K{!-;ldN{irZJcUAO6N81*-Xc2cAwt!ySNs0mCe*JunJioX1bZE%v>S3ybtJG*&TbuXR`gus14bar z2uP{khISVwQ5h1bL5PGUUqZfjUO|JNy@q|1AVJDdA3-?aT;>)1T+iSLj6dhjQb@QD z9Xwb3yD`%%y|i)EyP0KDPv;{V-*Cjy{XY~pJFd?0rR|}^N!ir?HAe2|@PE(o-MYT*Qtcf!1pUFUQ zQlfVMnZmhF8q(RwnS&x|xjhkqu14(Ih=g?;#t_(YQDQ?Od3?VYJRPk$+7LeNB&8JOfEXi5QvV@ zZ(*@agQ@>LM?;7V6^SaDQ6}6BhnAsx*mTkVF1k2Uh(&376EP(dBfzq)RnBK|R zIO&AB9WqA!HweHIS`#GQ02d&^8=-JyOKZuPp*OE%Z6fRsiJ5@*} zrLFay0inLgu=xN+)0g9!&!o~~@3{LOcvo%QnrQ6Cn7TG%8k^!~@abaxO+VTS*5|EswY7NQ}AVM5TJ_+lpYrm|9! z{9~aKJk&JW4$mY34SRNv!}C2Z>3xcN6Nv{St>jEpGx8vJzwyvMyffEfKuyS(pm(46 zI9&v!=xlgPXarM9$aH`tZ+5r?Yy2CRR?7rJKWeF@c>x^O$0fV)q>l~zg?xH+G<;H` zy_A-DE3_xONTcKK_O8_jW@*uLiI&e=KIJ?CKMuVA>4_Fy4U-v|8CCikF{dVla5erQ z&_3BX9}nb2cK}VN3A}&Ma0|I7e&Gt+?`eOKpnB$d{ zB4H&9IPL!-#p-j)*MhO*lnDe7nUqXq=r8$18^oB=y~!8!uEYq_I2eY=X*f!IM~h!I zf%!Kq75uhe^~b9h%c-wJNIEvzHC%UH5E4x)`6)Gehnrj zpUVx2i< zQi40gcN5{$Z`CEWC*xUM>CS4Y+K+b8M$%}aNdBF5Gua( z1PMbCfNz{Wr`Lzd=f{0S?OOqDHiatmqx$`IIyNj*#mZ_n>DiA-jvq40G!v>!i6Yi)wD0+sBg9UzjSCu0nVKv-S` z5V;y0R=qbq-8xX7{?{!LT>vdcMXKgS zt}locJUSR?Kdpb=1D(UC{%$84-i5ODSg!tMpnSP>y{>j{1d^LS#?~$APV8XZTz?Sb zw^QcTF34s-C(p0>hOh$nfN>~IReI8ge%Dk`-j|7u*ZQ=}CyYP>2$;g$jIj;@B?cZx z6R2kJEbhYJ37cl{rNJ}$byu&&x4g;UKEh_)u{)G>1v7qigzNPR0k9pIwE{4JI@qf! zQ>jOa;3UH;8sW)YlL`SDC__G!Fi$)pSRVdn2AUJofBE<_xXxX2tW%G-MvMGKVFB3OoKdfik79Fr{6z+ z`pQHoIpW5(Me^Zo@>@@AvDsN?*f za_suFBn{mU#?>B)AQt{tPo4kRMctP(`CB#r(Qg@s`6sNG#-wd}1;X9n0#0;Yr!;ck zq%M;{b6Yusz?jxuO(15CNOPN;K|4g!M6I`|(%q{E&F9n2hJ~{?;cKiPOjzYf(^BlT ztkD^DviF4UVOa@;UNAdwwDF~Eb|0qUM`8^_D~1A2#O)aYtc%XC`suyJes@#_oS(w- zr7~3^a*RHDByxAMg2O>pqyBUo=533#wi{Rr;t#$9)k;oh?H^%7-6%}-TnSshoY0J5 zpN$r&5&0^S&EIO^e-OaFAA#V=^j=)zd0~ym zAa?pzEG!<2i+?KgBK$B0DHHWAs+#}UHRG!@q_PFAV0wNmW#Ca*&`qZ2=S{*Ore$r$ z?cTj1o@+@L0aDfGczXM;b1BlsDnBK#WGagt0sfw~APgA)@&5`6Z|+xewJTDu+;Gh| zvg%XdSO0RgUe^aC#qW%s8Cc_!W?uGWLV`liF}Ud2BJJp75V*$ks&lQvSTs@d8)8iP zIeyYMb>1VNf$zuJUd-W`y|*PxYsMK6H!QC(5SBNw@UF>ri1WusfNbj{I~zoJ+j`e8 zq<^lM?}xl|^#zj>it#Ge@@bTrA;NAb7xi81rw>@?Q^H^edm&? zdlJc;OL>$w?!w^Wof#rwCv*Uj%PtGHMIM_nZ}DgS?C-R5?yP{>*YDBh-B_hmcrkFf z^pa_y6WtOq^NXqU6YJqtHx@@l`Os&q!(PF=3P!{~K`eYDAT7#XQcC`#DEb9BWG1Pg+84x1KGNTV* zwXwnzQrf=-E_mL8z9}h-F2e(s%ON6xcjPD--eXx?^7>ba>ev{4^Q%{ zdbghHYHQlOB`2uz%KyV=kiUV}g<>^()`UA2D0S1;Yft5noSvbYM-@T$f&ZuF1I2b* zcsXd@*J}v%j%>1}wj+C>d#LlS(&THB%2~Wn3a11ZGrB--q&u zI<|lQPrN=nGL*w!Prrxn-%*K|^CqV$#U+NB!mi5RG@tQW$gCO5wnGrO>7hv;Ga=J1 zuc=VZzI(yo9FzR^3H_kJs{{)h_;V#6qB$!J$j+s;ZKhIJf@7kUFsBS5KylYYTR#+$ z9Ss=!dqr@iVJK4IJSzUxb9@hp5#?%L;7T-y$Dn0)eH~$J;Ql z!(JXKj^A)phKb+=Q(6yAzGU&NOwdeiP^3kyUl8WqKeND0^(C2hTy2kOPBJa?foThB zzL!kdf%4LtKu6_+c#fx}?Z4|bLaG|QGb}4eXpvOP3IjqE?s~Q68GHk^;K0CCV>s{r zONxBXoE!qADvQ7ugf_eF`jB1iM`VW?5r0UeuH&k;1uo$dj6x+k-3{mZHK?NzC_#c> z>u~IhM1Sble8x`U3sMh{zCWb3S{sO34AAt9@qDHV%euu>!Da9dxwmRT?a7laeSG@l z^B)>bE1dYnwVZL$gCpVJF=gtdPaHFKe(l0wYv^_O{5=VuICuAT@73QRMPZ9wp?_b0 z)!d!&>Ef$-fq>K`T}>%b1QxiK9zAu6acW8xWU>$(B(Qv2=CuGJ)&+bIhJOE}Yfo;? zWS9E4wY0k0Mue)@{9r(0;o$X$8#Z)q?cXk+`;Pz^{nyARG}Yt^x(stBzSpO#<}dzM zs`VsVZpyD}XG639Cs17Or|(sw^kh08;f*E!6eY~ssq-ISI3dMNh~2sUS2d@ZPT(KH z3ZCFTM-MvV`g3WyeEY(0B^3dSn#SrsYe~Tt>nf<3K8fLc42E4BA4M4$kwZXHZ^r1r z#S$?q2c$UHaEW-c-U$nxILv>Xk1@`rI0buGofpS6p@g@s72zElMbW`i0TAs*Yv1_( zB~=>|>A<#eo4#X6`EA!^Dv<(7H?Fjmb8|pDC&@s`SAD?} z>RWn+@w}(toBAm}sh^sTy-$oNs;`>&7)&b`H{;9biP??qse)2Gc91QKK7kiv3Z%OS z3pWr4z8GRO$wbbF1m6{6m~Pv+XwDKJs^=;*hycdgt*HuDT~khq{37J7Rr3~CMn)Fh z-&B2KM+V7HswsRGufrKu;oE}ERNToTsrtGzlSC5p;NQB>v-xWZ^{$GBI!V~%<-(7c zO>8~zmZ+WUoegI;fy%eGrM*lj1J3^vJ^-n`yYAxEw5qUvTRe6C;=d@zxN+l+vp@6> z3N0tkd*q)D8T>gy!eO5&qu5K6U+?J5OigZ-NhwQ&Y0M8e>Qq=aJPoheXF7GoD}yck zX)lrD^62LvO!yaU?)zOl?k|%6k5|lx+k09=O7(0iNMw@%5iLs}SUvh*=X}Gqu?jH* zx}+^?MFVz`R)HR_0o&Bk9upI!(Isvt>613Wu;@cD_`6>Z;fV({RtsYsK!|-&8X|r} zOvXe}25+|>Y>;;(&bl3)i65cR4x^BP^yyt0iNn!jaNa`)-G})J^7Ov6zh2yrB}_&K zT#Y+HAy?e%-4*%ti>|3~2Yf+Rh@wNLBPv$7!HDX65@~JbHEYTZss2-%MbeeBCsHWM zJTw7glf9#zapfcleK^VDd6OMlkj`oIYksX&WC;e>q<4`|w4?68)t%WeM?zZhIWYd8 zi#Z|gf0x~tpFT0tpwI%rWAnOh5_^+e#rQ1*g16K&p@W%8rujk;~{B##oHV zw;-UxMUbw(gT=>ojJdr9Qkd*ajQsZQh>qUUwCK!T$+hB5ZaBfMQ^JNBr8P0F`1$?G zQM$u)ieKq}IUDC;QTjoV4sC^MYKC`H6~-LLhDg_3o{c_no7l`FysgO&eBuQ}uskKL zc^q>!`0p}Uv795Ned^Q&i+bAc9ow#i= z1uL=7N)64549qDfqC22zpvkKbRUkE$$@1>$`7X&tqSWGtHVYFa* ziq$YJL0#r@nv~%xEPqEs>xjr@xCO5V@4jUknXz)PBb#NwNGWJWRv8d_k6Qj-3r;+i z2GKbu6q%{Jp22>rj{9jbLMIi=)^?XqP$dBa1}S3$!m+UOjqwi3l#aF!!ob@z@tKY= zmE(*>@UEU~u%#?jQclDZjjiM`gJOM8TAazm5_Z#pn`7`!fH~@7m{0%>9fbSDjpiezSoF9>ca@(j7W`95i92QB-Wj-dSZ+bT9#%|gquDTcI!@Im5se)^@S5K z$q${(vf`738T_KB+;jhC37J5^lqz)gtQ!4T-CS9)T96Hk=Du3BV9DipZeRfM@Vp!u z`r}IHU@)|U*AmP{-!~&(W}mN-;8;N;BlJZSp4jKsxcUqrpp^Q+uzC+dDKzTZc;n?T zjKksVdU)SKEmj})o)IeQd~7J|L0riS12U?;Wn**MnDt=jCwe8BbtiMjUa3Zx?MMMR z&xGjaEJrIe$$p&7qlZSnAR}P$4j!Ly;YAAf%Xw7TM5(d@iG{Mwhglr z)$7(>u+x7$KoYv;ek zBORvHH;L-%*qSxTPV_8TlPt&C2Seb%hoQE>*lDmLUH5vlK?&p?I?Jp%K{0Mb7ck4s=2bPBB1Sqo3z>V z4$LMH+Hn|2E~R)G(#Q)v0i7h#HryZ$<4sTM^QaO-slP+5Pm7DbLbX)uCpRAzD)=!y zRd_O0jskv6xuoU}!?dpTd~(J7oso4yroiHM;v&%Hzxl}eMRQXQr_BsVTxbjAVqI;Y zRlv?zx7w2J-Jy4WG*&ozKC}|>#PL8{k^liTfT6wPkGQ?zYaa+~{8hkd=WTP_Dlc4e zS;z=alvdF<3wH5Fnh#RhI?xsO}P8|l@`bXM{JW2bt^tIr>>Pa_IA2Gf)IVEOR_c%$nAB9qW)72-mD5d`kJRu^Tk#i0wMj$Z+ zV1^HEt^RG}qSMwSRw90`!k;DL_nWUKhyZCnV7GfAIfZ3Bxpv_ClNBQ;6+8n%*t+{* zlvSQCZ08m?LR6tl3Z(e}Q>ASC?nrs@1%rhR%=kOT4SI^ehU-pShsDgj?)&R$&AC@u z{l(u6!$kCTM8Zm+IMxRSk>J?_b)|{!F!zg)1L~Nz=6TAFTnclgkQ*5%_sJfZLM%EE zSOGB-q6-7j(hG+Q%Q$dFd?#NJFtv3X%E6&;Ji9$ zzY)nOO$Nkevec@&>bZSPgLARZ!-}Tz)~oP*m$(bdVb$?m?5y5~Z6i8h@|cUnL6C?* z8PVv9Xp!+7QN2IXYpTRD1ClCo!=T)(JJ(N!-eVqCkC=JVo@0|jj2x9=pW5hH5k%=K zY$x>oz)*ypjn~gvh=J%Q2t~M+oW1ffxeHLO+sRRPV_Dth^*2Le%&FiJSbzQZHw#Do zmYf<2-4x4k0(CGe+KsTmsXteecxtr+I)?XCuf)HSW<}*{L^9eja5SUD+IEFlvaq8X zCWb_Anqh<=NagqE&K$+(lR&YZ4&W9;<+oV_1x_#}r zGZTu^l|lY`t#lU-TRaCQtLd_KGjvbtJgZ!-SxK4bGjTXSBrQ9$7gwVs$&Nu#*M9A*S@Q( zYq_!>r3IrlEN_v%59hVEudo4NV9j~?h&zf$LSr{@fl>dh?IY15mL7&)c>sK$!JXzTDpixL<|->x0zJpB7TNJH>c z9){+t#lCYLT?msPbddaKQ1G+X*lgX@aTfEk~=` z#s}BclAD>0A{#Dmjs)do2~yR)eW@7hZpAUIb8xlbUPiaFmfwTDwdP>o89qGIq+5X+ zce?`Vw$QMFwy%X|cq-bQm9vO>*RMp_lBJsc*fOaEY^%BMQTnmn$oQNRI#_#a`0jiu z@rd#E9*e@AP6u*k{>q=0odS=oh+;#oXa!L;|D zoKqzE#M8<>7p{+nGX*?p>?tc)kQ4d&Pp|R`0+T#gfe0{pvR4-CCA|NTkeW5&0UKixv#ZZq?t>_kFszxwOF~R^)HTgp^!QXnPY#yxu2G|HB^uKOhbECi7Qzh zlGH59UhMo?7)Ne8v>mM@ql}a=vw@nB-jt-~3*2E7vyBaF+6@E)O~Rm*2Iwy%m|%N( z!=*d~Nn&E}xw{p;LT$r3#8FV}(Mjd6f0Xu3&G6GUpHq>P=d4oua0)N0w_E?Tjqs_S zSOo>$ZX~nqc#3?p)H^>&i8^R{snZr-kERQ=yY{_AU~X8IJU5je%xX-{YfOS^ay2L% zCzilr%U*lJ?mE(maJ!h^=JTZirKj*QDAhu@-L&=n{&>Q7=b`hmrlx83mU)`(FeBEC zS$X?JeE#m*c~@5v{K{*~&1+jUjy6)lNy*p2g?Qk#1}MW=W!iNmV@QAAJ*%??xmCJS zgY0SDr=pU}-pK2*k#mDmlI*dvFVs-pK)FoTB~*Ah$A51h-5MoPj_24*!nCanxXndr zYE-Pj6RJ*tArTf{T?nF>GBZEqfE$yNI;mR)mqByOA`)9K}@(W_g z87GN*BPkr8VTn~fhY6pDo8hljA_UBzBVOkHU~Hc;R@RjA#0Q@C$dq2)Z$*{@uL3hL~=kF10>1lS*So6gS0JxGFH1sMHY|5kqSE zbg}(Tzv>!OJLe8%{RFhd8qdip)ES3X2+|I3GX;E5xyN+fr@b49llxi&QKX#WbGuwB zMV8MD#%LoMo&&`Kn}62K5Hf$qz}^pQo8!1{QBv{QxCF1B(%AW&A{MeON2={k7Bt+Q z5_hOfK3KAonqon#u%MmnITxu@-sJrytyeVjebZ~H_yldoYprojAuHD7wMS`3jt5AS zMknNaMch$hI=2YIIrH(~;lX$Y7CQuWURik;c)AWyt~J3#<>mAeBU~$O?kj)dWRaTT z>I-w&W;928Q=HrC>Q9WfB$3~vgN-YDf31ZN9>I~Hk*j>9xCx+>xIzv!c%xd`bFbLf*7WGEBdy*@ zhNfw1=D}m6B{Gq=lhbGLhPF7Z+4(&>BRG8201@FRPm>Sx7v&a3VIO{mIBCUE6s^!# za;m&&{ygcZ>;eb>!5McCb^o7UZk8KbfvAedv<#*l)QVc}R%KB^nZw$7UJK(8+wCLd zT@Qa;uj%S7ttxsxbhaN8*J+I9CNhzo(cb+LYnb`;pv_g|c+Pj_J-4S2iiq1tr`s?H zky=8L4Cj1`c=kif)X$AA{z{vNyemdj+DynD<4T$Jxxs`8-qGzsZ56S}5>2E_RDM5c zM@OU0ifJYK)J?8a9Cz$nhyg6wHJ>Rk-Z+Fn8yeFYn2#N}bND&A2EDntRd4#LU2l8< z=0tJsc;7uw4QMqvrNI_vHv7FBlbH$MHMK5PH5W0$A?uPC=r2wSIwHdh?VR>R-cGUo zF?4bEVWvuaFWzNm@mL~hjE?lkW#>+JjkJcSd%&;@_fdLWy-6%!GG$+XoyIyfHx>x> zWWg(Rme^VueDu7n7`xKmzMiR1w-7C!!lnC!URcES+d^poV~Y^${Z~RnudM3UW3Mb- zG}X}RN>ASmBGdxN;NR8GRSeJV$$c?LPg8 z@2;>}SFV3{m;@1Kec3x7S_5@Jc)CHx<;ob!zuz$2oWv{#Bet3TUSamuYI%rRG`Ax- zV9CcaHVQab?eHt;<-|~DB6)D4C7Nwj!q+7!1K*{Yg(aT&vUg8%E0V+~YS5ucGR_-A z7U+BT>2Ge3S}T)SqLT!?@sSh^;VQCvH~65MWqkeUq>uCC8X+2Sdb>s9cDd$8I!v}& zG6N0P_U*n0irGR2REt%HvTrSCYxx#D`=-6IZ?rQpIL=$E@RSXn%Gb**#W3>lM=Y#) z-}l-%CcMihJ(#ewM0WUgFn;uPTnHq(&*p%-75$3|D_YB0_%{l=ipiyy9zMI`k?pwM zGxwS@elMW)QQanp+k3GcHWI?lZh;0mx`MnS9)XT;3e>!TM^nA>LYrqp>lpg}`aZ&k zzxq4`HLuNj*p*QIq{ZckoD^rR;7DM^F(1W1OB`dq0POvub;gt7ez5EJ!SsGuM1H!Z z=t7ckyXo-HvDO!ryIGt8GGg45$G3YP-bREUEn_I_E+lZ5>s64jQl)5ts1{cnFJ$Kq zPn1dm8?ROFor-H87StNEv(B%Oa#IqtXG30;2zJ29PU6NuhkN0I`!Yx(FZB6y7ZQxI z{IIliiDKBH-?&()6>E^CKXzFi1m*8jm7MD;Wy6Fm#~OqcJu((Iz9k%cir0D;m1kJu zgymihWhpl*i=rf4X2ACpHENT3o6B)CGEZPq6)FB|IPx`oljc032o@iv-(yBc#-P(P zTX3mSh3}={Kly|6u=Y-TJ7hMUW5_AN2TRb6JZ&6(Ix#fD9Mg$qyCTNYm?hpCG~b$5 zDOlhz89=id=Z=rAM74e_)x%Yz{fd`%(Exb1UtEtMg(xdL=HOkOkkqZ7b95$tbI1wr z^ZK*5&k{0^R$@ZPU?F?7svJ43#wOsa{=nL6j^gF*0~~6r?=iR`lu7ItolSZnz`MBl zbt#zeN{`lqMKwY3BWxjFU+uS|e#-6Rry8sC6_mE)FAb*2h1{$*x=D>?Jooz}1811L z84Xx&Po47ar|tbeuctsqqXRYf%9`8(+edPDD8Iv<31ITXC@VNt{46*!z@DHi1kSc{ zx3!U&YLCOqV9P(hHpJ|)lk^}?RT#}q$7_xco|4>VpeW>NUX#*7z3fsa>$ig2xLi@M z2n_#-^%LPxX;Z56ov*&p<11)!iXHJ83*lhkEO*hJqoSUsQOcg<&ZCgVCk%WHm(l>s zq91E!-tY9{vm->nQN=g{$h7Wr>nWzy9fB~D;hlE~XNX1lUMIs(gv=SCm9@F9jy8zD zwE6&|<2y2ZVVhyAzwdt(j`raGr$Z$4b8UN;QEbyZPTyC0y_&{J$!q4Gw~Z#u8`(OX zIABU;_36ox%|4d&^>F@5cnYBg(4**W@rF{zOPk!vjp;2IXT(&MP1M1?AaCWXf)T@D zhFK)->(qK!XIlK2-ak2OGI?a!HPbL0MqL&I- zRGol-0iT550ALdnky5fgV1D=M2#n*meaQ_D96fzQBpLF#=3Qaq#!nbcgZ}eblEYu3 zq<~MYgMq#}YdXa6QhvBbNJRfDT+sZ5Ohj>#uC}B>K`e`b4*9Wq;?+3TV);l#pDSU- zzCBtrfZ%0`&RD^HwoIAPsq}vp{}nID{98>#Vek1ee%T;Z`~hI?B0pHwtO_p#QX?7S zd?VjZ$1nJ?A-RA}fXM^KI1`C3+ur*d!37`wSSwM)qweDHiw8yB9M1{y$voG>PSmEv zg^|EP!*Xt|G0kw*Cr^#FPf;1I#|9rC^3&0ZQm~_gty2xe_Ut_$ghqYCK>rm;vV3i7UZ&GN}G?(oS7BF80C$>}_oHKUe=QfPmcN7qXxu4c&VIHuh?pE2G?xrqx30eEm9B=>WZk zEY?lKMDmvx%*r#GShSZ@>3Qtf5qe1cyNyLph;)-gT1ZG0mgooPQ{o)~F+i%iEks=V zm57j5iDPsVZScuc`C0GY_45AYFvqFyImjj&0&=6O)&y>&_13h?oVJn(1zaTLMdP>j zw;@(Mup?OSSH9TMjHtuAK{}fzo#&Z=$X`f2drfoqHfgnO5yyqXK7C*igcQ=Xzf`9q zdF3NSkNC?*MxoYXgsrghBATQkpk7zxoj}a1t3IVl(1RW_@_|bcAsN4B< z7{}X5OkyTnNGNbTelDK!5*5`(hc6x}i_+5h{Oe|>NLB4(Dwsa=71C85xebB%GV`|F z0(TJJbK9Og{5EcNRKQTQtU_~^aQkkQ^;Ov#-plcB;_^fi)X-0&jLtDhB*|MuRpc~I zgbZDdG{Djrov=WC2=%VdDOG7II+;+L?2BQk+J$g6&Yj{u7&$KA-=GHCu zSfQ74E9b#z=FbwEAMOdcf48|P-0~=|ZR7e*6uff6#yg>+{MuR?qeacm8pMKxccoU% zz~G$3uhRl9IKvTP`PK>uQzpN*C0QbHwatga=CHag6Uc77XuLz_%nR)ARl>@k0*hK>R zk#$c9n06Dy>QB+gP@TPA{-zyoQt)d)9>#bGB6{~HHbl3J<%ajN_Ob3Z{Tk|E0pWD# z;&4+a*%xw;tM(NHw$jKI`Qo+|noyKykH?O0*!C;=(wQPTN&IcmQ~wGl3sE62(T|4r z^72|>LA+9@(e$;524M5YD<~zwcwtQ)F~%bJ2eUteaYR3yx@?aMdNjMyj!1qM8@RjAHUp+`n|v!Wvy;SXSffn^sSKO6&Z1i>YB=g1 zbZn_V1^w-;2vIg9DEuz6R6E(ziSgHx$u3hXNY=DK@sOwE_$DrC=}kq$075h_>aQkT zChvk~pbVHi@U4RYXlP=WyB%c{psB5R@EVnbP!I2=uYliBVigt_57~7NxT~37!i)p) z2Z{0VB7*>lCgcYe=rRdu_m|z@2J~RSTZlpi5Ti?&_H3wfl|oo`Zrn)+Oq9e0Cz=*% z@+G3UW@Ffjx~x&Kqy264pK!l*q|6zCg|9|KnRaX#1YJ?sH5~IU=~;gz#J`uNzu(>_ b<6J*7n}6qGd68xd_w>kul_V>~jRO7y$ Date: Fri, 9 Oct 2015 22:04:04 -0400 Subject: [PATCH 07/14] Make the logo on README smaller Looks better a bit smaller. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25ffac9..55ad905 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Giftwrap

+
Giftwrap

A tool for creating bespoke system-native OpenStack artifacts. From a4d8caf302d189caea20c84b37d556f2f503d646 Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Fri, 9 Oct 2015 22:55:52 -0400 Subject: [PATCH 08/14] Add parallel builds This change runs builds in parallel. By default all projects run simultaneously unless --synchronous is specified. This should speed up build times dramatically. --- giftwrap/build_spec.py | 3 ++- giftwrap/builders/__init__.py | 14 +++++++++++++- giftwrap/settings.py | 4 +++- giftwrap/shell.py | 4 +++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/giftwrap/build_spec.py b/giftwrap/build_spec.py index c0a7e2b..dbcb555 100644 --- a/giftwrap/build_spec.py +++ b/giftwrap/build_spec.py @@ -22,7 +22,7 @@ from giftwrap.settings import Settings class BuildSpec(object): - def __init__(self, manifest, version, build_type=None): + def __init__(self, manifest, version, build_type=None, parallel=True): self._manifest = yaml.load(manifest) self.version = version self.build_type = build_type @@ -31,6 +31,7 @@ class BuildSpec(object): manifest_settings['version'] = version if build_type: manifest_settings['build_type'] = build_type + manifest_settings['parallel_build'] = parallel self.settings = Settings.factory(manifest_settings) self.projects = self._render_projects() diff --git a/giftwrap/builders/__init__.py b/giftwrap/builders/__init__.py index 666507a..fede4c1 100644 --- a/giftwrap/builders/__init__.py +++ b/giftwrap/builders/__init__.py @@ -16,6 +16,7 @@ import logging import os +import threading from giftwrap.gerrit import GerritReview @@ -84,8 +85,19 @@ class Builder(object): self._temp_src_dir = os.path.join(self._temp_dir, 'src') LOG.debug("Temporary working directory: %s", self._temp_dir) + threads = [] for project in spec.projects: - self._build_project(project) + if spec.settings.parallel_build: + t = threading.Thread(target=self._build_project, + args=(project,)) + threads.append(t) + t.start() + else: + self._build_project(project) + + if spec.settings.parallel_build: + for thread in threads: + thread.join() self._finalize_build() diff --git a/giftwrap/settings.py b/giftwrap/settings.py index 6175b2c..d6962e1 100644 --- a/giftwrap/settings.py +++ b/giftwrap/settings.py @@ -30,7 +30,8 @@ class Settings(object): def __init__(self, build_type=DEFAULT_BUILD_TYPE, package_name_format=None, version=None, base_path=None, install_path=None, gerrit_dependencies=True, - force_overwrite=False, output_dir=None, include_config=True): + force_overwrite=False, output_dir=None, include_config=True, + parallel_build=True): if not version: raise Exception("'version' is a required settings") self.build_type = build_type @@ -42,6 +43,7 @@ class Settings(object): self.force_overwrite = force_overwrite self._output_dir = output_dir self.include_config = include_config + self.parallel_build = parallel_build @property def package_name_format(self): diff --git a/giftwrap/shell.py b/giftwrap/shell.py index a461fed..f46ce1c 100644 --- a/giftwrap/shell.py +++ b/giftwrap/shell.py @@ -46,7 +46,7 @@ def build(args): with open(args.manifest, 'r') as fh: manifest = fh.read() - buildspec = BuildSpec(manifest, args.version, args.type) + buildspec = BuildSpec(manifest, args.version, args.type, args.parallel) builder = BuilderFactory.create_builder(args.type, buildspec) def _signal_handler(*args): @@ -80,6 +80,8 @@ def main(): build_subcmd.add_argument('-v', '--version') build_subcmd.add_argument('-t', '--type', choices=('docker', 'package'), required=True) + build_subcmd.add_argument('-s', '--synchronous', dest='parallel', + action='store_false') build_subcmd.set_defaults(func=build) args = parser.parse_args() From 8dd2e05a303efca3c4a2a30da0eeda423acb43bf Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Fri, 9 Oct 2015 23:02:12 -0400 Subject: [PATCH 09/14] Only attempt copy when directory exists This bug is the result of poor whitespace management. Only copy etc if etc is present. --- giftwrap/builders/package_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/giftwrap/builders/package_builder.py b/giftwrap/builders/package_builder.py index b25c1c1..fde2472 100644 --- a/giftwrap/builders/package_builder.py +++ b/giftwrap/builders/package_builder.py @@ -82,7 +82,7 @@ class PackageBuilder(Builder): else: LOG.debug("Copying config from '%s' to '%s'", src_config, dest_config) - distutils.dir_util.copy_tree(src_config, dest_config) + distutils.dir_util.copy_tree(src_config, dest_config) def _install_project(self, venv_path, src_clone_dir): pip_path = self._get_venv_pip_path(venv_path) From 5df2dff2f257d85af41cc438f7ccdd9fa3389bd2 Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Fri, 9 Oct 2015 23:25:38 -0400 Subject: [PATCH 10/14] Support postinstall python dependencies There are cases where you would like for a Python module to be installed *after* the project itself has been installed. This is now possible with project.postinstall_dependencies. --- giftwrap/builders/__init__.py | 7 +++++++ giftwrap/builders/docker_builder.py | 5 +++++ giftwrap/builders/package_builder.py | 5 +++++ giftwrap/openstack_project.py | 4 +++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/giftwrap/builders/__init__.py b/giftwrap/builders/__init__.py index 666507a..c9f867a 100644 --- a/giftwrap/builders/__init__.py +++ b/giftwrap/builders/__init__.py @@ -71,6 +71,9 @@ class Builder(object): self._install_project(project.install_path, src_clone_dir) + if project.postinstall_dependencies: + self._install_postinstall_dependencies(project) + # finish up self._finalize_project_build(project) @@ -132,6 +135,10 @@ class Builder(object): def _install_project(self, venv_path, src_clone_dir): return + @abstractmethod + def _install_postinstall_dependencies(self, project): + return + @abstractmethod def _finalize_project_build(self, project): return diff --git a/giftwrap/builders/docker_builder.py b/giftwrap/builders/docker_builder.py index 28c3d9d..8ea99f3 100644 --- a/giftwrap/builders/docker_builder.py +++ b/giftwrap/builders/docker_builder.py @@ -97,6 +97,11 @@ class DockerBuilder(Builder): pip_path = self._get_venv_pip_path(venv_path) self._execute("%s install %s" % (pip_path, src_clone_dir)) + def _install_postinstall_dependencies(self, project): + pip_path = self._get_venv_pip_path(project.install_path) + dependencies = " ".join(project.postinstall_dependencies) + self._execute("%s install %s" % (pip_path, dependencies)) + def _finalize_project_build(self, project): self._commands.append("rm -rf %s" % self._temp_dir) for command in self._commands: diff --git a/giftwrap/builders/package_builder.py b/giftwrap/builders/package_builder.py index b25c1c1..22b997e 100644 --- a/giftwrap/builders/package_builder.py +++ b/giftwrap/builders/package_builder.py @@ -88,6 +88,11 @@ class PackageBuilder(Builder): pip_path = self._get_venv_pip_path(venv_path) self._execute("%s install %s" % (pip_path, src_clone_dir)) + def _install_postinstall_dependencies(self, project): + pip_path = self._get_venv_pip_path(project.install_path) + dependencies = " ".join(project.postinstall_dependencies) + self._execute("%s install %s" % (pip_path, dependencies)) + def _finalize_project_build(self, project): # build the package pkg = Package(project.package_name, project.version, diff --git a/giftwrap/openstack_project.py b/giftwrap/openstack_project.py index ff7ef38..d99ba4e 100644 --- a/giftwrap/openstack_project.py +++ b/giftwrap/openstack_project.py @@ -34,7 +34,8 @@ class OpenstackProject(object): def __init__(self, settings, name, version=None, gitref=None, giturl=None, gitdepth=1, venv_command=None, install_command=None, install_path=None, package_name=None, stackforge=False, - system_dependencies=[], pip_dependencies=[]): + system_dependencies=[], pip_dependencies=[], + postinstall_dependencies=[]): self._settings = settings self.name = name self._version = version @@ -49,6 +50,7 @@ class OpenstackProject(object): self._git_path = None self.system_dependencies = system_dependencies self.pip_dependencies = pip_dependencies + self.postinstall_dependencies = postinstall_dependencies @property def version(self): From 3c0f88249ac73be6ff80b61f9b1150773f0eaa33 Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Mon, 12 Oct 2015 11:48:19 -0400 Subject: [PATCH 11/14] Docker builds need to be synchronous There is no point in making Docker builds "parallel" as they are executed by way of a docker build. --- giftwrap/build_spec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/giftwrap/build_spec.py b/giftwrap/build_spec.py index dbcb555..2989eb2 100644 --- a/giftwrap/build_spec.py +++ b/giftwrap/build_spec.py @@ -31,6 +31,8 @@ class BuildSpec(object): manifest_settings['version'] = version if build_type: manifest_settings['build_type'] = build_type + if build_type == 'docker': + parallel = False manifest_settings['parallel_build'] = parallel self.settings = Settings.factory(manifest_settings) self.projects = self._render_projects() From 37eed319e5f28e8ed00736efd7d3a55275982776 Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Wed, 14 Oct 2015 21:01:50 -0400 Subject: [PATCH 12/14] Install pip dependencies iteratively Installing pip dependencies interatively (vs. concurrently) provides for interesting manifest syntax. For example, one could specify an index-url along side a dependency without affecting the other dependencies in the list. --- giftwrap/builders/__init__.py | 23 ++++++++++------------- giftwrap/builders/docker_builder.py | 8 ++------ giftwrap/builders/package_builder.py | 8 ++------ 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/giftwrap/builders/__init__.py b/giftwrap/builders/__init__.py index 617cd79..4e68195 100644 --- a/giftwrap/builders/__init__.py +++ b/giftwrap/builders/__init__.py @@ -39,11 +39,11 @@ class Builder(object): def _get_gerrit_dependencies(self, repo, project): try: review = GerritReview(repo.head.change_id, project.git_path) - return review.build_pip_dependencies(string=True) + return review.build_pip_dependencies() except Exception as e: LOG.warning("Could not install gerrit dependencies!!! " "Error was: %s", e) - return "" + return [] def _build_project(self, project): self._prepare_project_build(project) @@ -57,15 +57,15 @@ class Builder(object): # create and build the virtualenv self._create_virtualenv(project.venv_command, project.install_path) - dependencies = "" + dependencies = [] if project.pip_dependencies: - dependencies = " ".join(project.pip_dependencies) + dependencies = project.pip_dependencies if self._spec.settings.gerrit_dependencies: - dependencies = "%s %s" % (dependencies, - self._get_gerrit_dependencies(repo, - project)) + dependencies += self._get_gerrit_dependencies(repo, project) + if len(dependencies): - self._install_pip_dependencies(project.install_path, dependencies) + self._install_pip_dependencies(project.install_path, + dependencies) if self._spec.settings.include_config: self._copy_sample_config(src_clone_dir, project) @@ -73,7 +73,8 @@ class Builder(object): self._install_project(project.install_path, src_clone_dir) if project.postinstall_dependencies: - self._install_postinstall_dependencies(project) + dependencies = project.postinstall_dependencies + self._install_pip_dependencies(project, dependencies) # finish up self._finalize_project_build(project) @@ -147,10 +148,6 @@ class Builder(object): def _install_project(self, venv_path, src_clone_dir): return - @abstractmethod - def _install_postinstall_dependencies(self, project): - return - @abstractmethod def _finalize_project_build(self, project): return diff --git a/giftwrap/builders/docker_builder.py b/giftwrap/builders/docker_builder.py index 8ea99f3..c8e0d83 100644 --- a/giftwrap/builders/docker_builder.py +++ b/giftwrap/builders/docker_builder.py @@ -84,7 +84,8 @@ class DockerBuilder(Builder): def _install_pip_dependencies(self, venv_path, dependencies): pip_path = self._get_venv_pip_path(venv_path) - self._execute("%s install %s" % (pip_path, dependencies)) + for dependency in dependencies: + self._execute("%s install %s" % (pip_path, dependency)) def _copy_sample_config(self, src_clone_dir, project): src_config = os.path.join(src_clone_dir, 'etc') @@ -97,11 +98,6 @@ class DockerBuilder(Builder): pip_path = self._get_venv_pip_path(venv_path) self._execute("%s install %s" % (pip_path, src_clone_dir)) - def _install_postinstall_dependencies(self, project): - pip_path = self._get_venv_pip_path(project.install_path) - dependencies = " ".join(project.postinstall_dependencies) - self._execute("%s install %s" % (pip_path, dependencies)) - def _finalize_project_build(self, project): self._commands.append("rm -rf %s" % self._temp_dir) for command in self._commands: diff --git a/giftwrap/builders/package_builder.py b/giftwrap/builders/package_builder.py index fb49fa8..3b5ff79 100644 --- a/giftwrap/builders/package_builder.py +++ b/giftwrap/builders/package_builder.py @@ -70,7 +70,8 @@ class PackageBuilder(Builder): def _install_pip_dependencies(self, venv_path, dependencies): pip_path = self._get_venv_pip_path(venv_path) - self._execute("%s install %s" % (pip_path, dependencies)) + for dependency in dependencies: + self._execute("%s install %s" % (pip_path, dependency)) def _copy_sample_config(self, src_clone_dir, project): src_config = os.path.join(src_clone_dir, 'etc') @@ -88,11 +89,6 @@ class PackageBuilder(Builder): pip_path = self._get_venv_pip_path(venv_path) self._execute("%s install %s" % (pip_path, src_clone_dir)) - def _install_postinstall_dependencies(self, project): - pip_path = self._get_venv_pip_path(project.install_path) - dependencies = " ".join(project.postinstall_dependencies) - self._execute("%s install %s" % (pip_path, dependencies)) - def _finalize_project_build(self, project): # build the package pkg = Package(project.package_name, project.version, From c188d6238a12b53a45af9adf4ef7d935fa51ca26 Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Wed, 14 Oct 2015 21:06:46 -0400 Subject: [PATCH 13/14] Fix git depth issues Defaulting a getdepth to 1 makes it difficult to switch branches if the branch is not available in the shallow clone, so make this optional. Also, instantiate OpenstackGitRepo properly: depth is not the fourth parameter. --- giftwrap/builders/package_builder.py | 2 +- giftwrap/openstack_project.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/giftwrap/builders/package_builder.py b/giftwrap/builders/package_builder.py index fb49fa8..2fdc315 100644 --- a/giftwrap/builders/package_builder.py +++ b/giftwrap/builders/package_builder.py @@ -61,7 +61,7 @@ class PackageBuilder(Builder): def _clone_project(self, giturl, name, gitref, depth, path): LOG.info("Fetching source code for '%s'", name) - repo = OpenstackGitRepo(giturl, name, gitref, depth) + repo = OpenstackGitRepo(giturl, name, gitref, depth=depth) repo.clone(path) return repo diff --git a/giftwrap/openstack_project.py b/giftwrap/openstack_project.py index d99ba4e..9dd0092 100644 --- a/giftwrap/openstack_project.py +++ b/giftwrap/openstack_project.py @@ -32,7 +32,7 @@ TEMPLATE_VARS = ('name', 'version', 'gitref', 'stackforge') class OpenstackProject(object): def __init__(self, settings, name, version=None, gitref=None, giturl=None, - gitdepth=1, venv_command=None, install_command=None, + gitdepth=None, venv_command=None, install_command=None, install_path=None, package_name=None, stackforge=False, system_dependencies=[], pip_dependencies=[], postinstall_dependencies=[]): From 503d3f0b7b80045055bbca76879acea951850ebd Mon Sep 17 00:00:00 2001 From: Craig Tracey Date: Thu, 15 Oct 2015 12:18:46 -0400 Subject: [PATCH 14/14] Add some threading improvements Two improvements have been added: - display the name of the thread in the logging - record the thread exit status and return it upon completion --- giftwrap/builders/__init__.py | 65 ++++++++++++++++++++------------- giftwrap/shell.py | 8 ++-- giftwrap/tests/test_log.py | 1 + giftwrap/tests/test_settings.py | 1 + 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/giftwrap/builders/__init__.py b/giftwrap/builders/__init__.py index 4e68195..77c87c4 100644 --- a/giftwrap/builders/__init__.py +++ b/giftwrap/builders/__init__.py @@ -32,6 +32,7 @@ class Builder(object): self._temp_dir = None self._temp_src_dir = None self._spec = spec + self._thread_exit = [] def _get_venv_pip_path(self, venv_path): return os.path.join(venv_path, 'bin/pip') @@ -46,38 +47,44 @@ class Builder(object): return [] def _build_project(self, project): - self._prepare_project_build(project) - self._make_dir(project.install_path) + try: + self._prepare_project_build(project) + self._make_dir(project.install_path) - # clone the source - src_clone_dir = os.path.join(self._temp_src_dir, project.name) - repo = self._clone_project(project.giturl, project.name, - project.gitref, project.gitdepth, - src_clone_dir) + # clone the source + src_clone_dir = os.path.join(self._temp_src_dir, project.name) + repo = self._clone_project(project.giturl, project.name, + project.gitref, project.gitdepth, + src_clone_dir) - # create and build the virtualenv - self._create_virtualenv(project.venv_command, project.install_path) - dependencies = [] - if project.pip_dependencies: - dependencies = project.pip_dependencies - if self._spec.settings.gerrit_dependencies: - dependencies += self._get_gerrit_dependencies(repo, project) + # create and build the virtualenv + self._create_virtualenv(project.venv_command, project.install_path) + dependencies = [] + if project.pip_dependencies: + dependencies = project.pip_dependencies + if self._spec.settings.gerrit_dependencies: + dependencies += self._get_gerrit_dependencies(repo, project) - if len(dependencies): - self._install_pip_dependencies(project.install_path, - dependencies) + if len(dependencies): + self._install_pip_dependencies(project.install_path, + dependencies) - if self._spec.settings.include_config: - self._copy_sample_config(src_clone_dir, project) + if self._spec.settings.include_config: + self._copy_sample_config(src_clone_dir, project) - self._install_project(project.install_path, src_clone_dir) + self._install_project(project.install_path, src_clone_dir) - if project.postinstall_dependencies: - dependencies = project.postinstall_dependencies - self._install_pip_dependencies(project, dependencies) + if project.postinstall_dependencies: + dependencies = project.postinstall_dependencies + self._install_pip_dependencies(project.install_path, + dependencies) - # finish up - self._finalize_project_build(project) + # finish up + self._finalize_project_build(project) + except Exception as e: + LOG.error("Oops. Problem building %s: %s", project.name, e) + self._thread_exit.append(-1) + self._thread_exit.append(0) def build(self): spec = self._spec @@ -93,17 +100,23 @@ class Builder(object): for project in spec.projects: if spec.settings.parallel_build: t = threading.Thread(target=self._build_project, - args=(project,)) + name=project.name, args=(project,)) threads.append(t) t.start() else: self._build_project(project) + rc = 0 if spec.settings.parallel_build: for thread in threads: thread.join() + for thread_exit in self._thread_exit: + if thread_exit != 0: + rc = thread_exit + self._finalize_build() + return rc def cleanup(self): self._cleanup_build() diff --git a/giftwrap/shell.py b/giftwrap/shell.py index f46ce1c..18d7019 100644 --- a/giftwrap/shell.py +++ b/giftwrap/shell.py @@ -30,8 +30,9 @@ def _setup_logger(level=logging.INFO): logger = logging.getLogger() logger.setLevel(level) log_handler = ColorStreamHandler(sys.stdout) - fmt = logging.Formatter(fmt='%(asctime)s %(name)s %(levelname)s: ' - '%(message)s', datefmt='%F %H:%M:%S') + fmt = logging.Formatter(fmt='%(asctime)s %(threadName)s %(name)s ' + '%(levelname)s: %(message)s', + datefmt='%F %H:%M:%S') log_handler.setFormatter(fmt) logger.addHandler(log_handler) @@ -55,7 +56,7 @@ def build(args): sys.exit() signal.signal(signal.SIGINT, _signal_handler) - builder.build() + rc = builder.build() except Exception as e: LOG.exception("Oops something went wrong: %s", e) fail = True @@ -63,6 +64,7 @@ def build(args): builder.cleanup() if fail: sys.exit(-1) + sys.exit(rc) def main(): diff --git a/giftwrap/tests/test_log.py b/giftwrap/tests/test_log.py index dd52cdf..1824615 100644 --- a/giftwrap/tests/test_log.py +++ b/giftwrap/tests/test_log.py @@ -22,6 +22,7 @@ from nose.plugins.logcapture import LogCapture class TestLog(unittest.TestCase): + def test_get_logger(self): lc = LogCapture() lc.begin() diff --git a/giftwrap/tests/test_settings.py b/giftwrap/tests/test_settings.py index 249b338..000b747 100644 --- a/giftwrap/tests/test_settings.py +++ b/giftwrap/tests/test_settings.py @@ -27,6 +27,7 @@ SAMPLE_SETTINGS = { class TestSettings(unittest.TestCase): + def test_factory(self): settings_dict = SAMPLE_SETTINGS s = settings.Settings.factory(settings_dict)