Begin building virtualenvs for each component

To get this to work you'll need to install the following before running:

sudo yum install libxslt-devel libxml2-devel mysql-devel \
                 postgresql-devel openldap-devel psmisc \
                 sqlite-devel libvirt-devel

To have anvil build virtualenv apply the jsonpatch file anvil includes
in conf/distros to the distro file using the --distros-patch option.

sudo ./smithy --action=prepare --distros-patch=conf/distros/venv-patch.json

A separate virtualenv will be built for every component listed in
personas file.  Requirements will not be shared across virtualenv.

o add --venv-deploy-dir option to make the virtualenv relocatable
  to a path different from the build path
o add --origins-patch to apply the jsonpatch file anvil includes
  in conf/origins to the origins file in order to skip bundling
  test requirements into the virtualenv

Change-Id: I47fdb68e71c3114f9cf441b3b33be9c7591356aa
This commit is contained in:
Joshua Harlow 2014-10-06 17:29:04 -07:00
parent b7719ce924
commit d435f54827
13 changed files with 276 additions and 13 deletions

View File

@ -96,7 +96,8 @@ def run(args):
ensure_anvil_dirs(root_dir) ensure_anvil_dirs(root_dir)
# Load the distro/s # Load the distro/s
possible_distros = distro.load(settings.DISTRO_DIR) possible_distros = distro.load(settings.DISTRO_DIR,
distros_patch=args.get('distros_patch'))
# Load + match the persona to the possible distros... # Load + match the persona to the possible distros...
try: try:
@ -104,7 +105,8 @@ def run(args):
except Exception as e: except Exception as e:
raise excp.OptionException("Error loading persona file: %s due to %s" % (persona_fn, e)) raise excp.OptionException("Error loading persona file: %s due to %s" % (persona_fn, e))
else: else:
dist = persona_obj.match(possible_distros, args['origins_fn']) dist = persona_obj.match(possible_distros, args['origins_fn'],
origins_patch=args.get('origins_patch'))
LOG.info('Persona selected distro: %s from %s possible distros', LOG.info('Persona selected distro: %s from %s possible distros',
colorizer.quote(dist.name), len(possible_distros)) colorizer.quote(dist.name), len(possible_distros))

View File

@ -192,7 +192,9 @@ class Action(object):
sibling_params['siblings'] = {} # This gets adjusted during construction sibling_params['siblings'] = {} # This gets adjusted during construction
sibling_params['passwords'] = self.passwords sibling_params['passwords'] = self.passwords
sibling_params['distro'] = self.distro sibling_params['distro'] = self.distro
sibling_params['options'] = self.config_loader.load(d_component, c) sibling_params['options'] = self.config_loader.load(
distro=d_component, component=c,
origins_patch=self.cli_opts.get('origins_patch'))
LOG.debug("Constructing %r %s siblings...", c, len(d_component.siblings)) LOG.debug("Constructing %r %s siblings...", c, len(d_component.siblings))
my_siblings = self._construct_siblings(c, d_component.siblings, sibling_params, sibling_instances) my_siblings = self._construct_siblings(c, d_component.siblings, sibling_params, sibling_instances)
@ -201,8 +203,9 @@ class Action(object):
# siblings get... # siblings get...
instance_params = dict(sibling_params) instance_params = dict(sibling_params)
instance_params['instances'] = instances instance_params['instances'] = instances
instance_params['options'] = self.config_loader.load(d_component, c, instance_params['options'] = self.config_loader.load(
persona) distro=d_component, component=c, persona=persona,
origins_patch=self.cli_opts.get('origins_patch'))
instance_params['siblings'] = my_siblings instance_params['siblings'] = my_siblings
instance_params = utils.merge_dicts(instance_params, self.cli_opts, preserve=True) instance_params = utils.merge_dicts(instance_params, self.cli_opts, preserve=True)
instances[c] = importer.construct_entry_point(d_component.entry_point, **instance_params) instances[c] = importer.construct_entry_point(d_component.entry_point, **instance_params)

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import jsonpatch
import re import re
from anvil import exceptions from anvil import exceptions
@ -66,7 +67,7 @@ class YamlMergeLoader(object):
persona_specific = persona.component_options.get(component, {}) persona_specific = persona.component_options.get(component, {})
self._base_loader.update_cache(conf, persona_specific) self._base_loader.update_cache(conf, persona_specific)
def load(self, distro, component, persona=None): def load(self, distro, component, persona=None, origins_patch=None):
# NOTE (vnovikov): applying takes place before loading reference links # NOTE (vnovikov): applying takes place before loading reference links
self._apply_persona(component, persona) self._apply_persona(component, persona)
@ -75,7 +76,11 @@ class YamlMergeLoader(object):
origins_opts = {} origins_opts = {}
if self._origins_path: if self._origins_path:
try: try:
origins_opts = utils.load_yaml(self._origins_path)[component] origins = utils.load_yaml(self._origins_path)
if origins_patch:
patch = jsonpatch.JsonPatch(origins_patch)
patch.apply(origins, in_place=True)
origins_opts = origins[component]
except KeyError: except KeyError:
pass pass
general_component_opts = self._base_loader.load('general') general_component_opts = self._base_loader.load('general')

View File

@ -18,6 +18,7 @@
import collections import collections
import copy import copy
import glob import glob
import jsonpatch
import os import os
import platform import platform
import re import re
@ -146,8 +147,14 @@ def _match_distros(distros):
return matches return matches
def load(path): def load(path, distros_patch=None):
"""Load configuration for all distros found in path.
:param path: path containing distro configuration in yaml format
:param distros_patch: distros file patch, jsonpath format (rfc6902)
"""
distro_possibles = [] distro_possibles = []
patch = jsonpatch.JsonPatch(distros_patch) if distros_patch else None
input_files = glob.glob(sh.joinpths(path, '*.yaml')) input_files = glob.glob(sh.joinpths(path, '*.yaml'))
if not input_files: if not input_files:
raise excp.ConfigException('Did not find any distro definition files in %r' % path) raise excp.ConfigException('Did not find any distro definition files in %r' % path)
@ -155,6 +162,9 @@ def load(path):
LOG.debug("Attempting to load distro definition from %r", fn) LOG.debug("Attempting to load distro definition from %r", fn)
try: try:
cls_kvs = utils.load_yaml(fn) cls_kvs = utils.load_yaml(fn)
# Apply any user specified patches to distros file
if patch:
patch.apply(cls_kvs, in_place=True)
except Exception as err: except Exception as err:
LOG.warning('Could not load distro definition from %r: %s', fn, err) LOG.warning('Could not load distro definition from %r: %s', fn, err)
else: else:

View File

@ -16,6 +16,7 @@
from StringIO import StringIO from StringIO import StringIO
import json
import multiprocessing import multiprocessing
import textwrap import textwrap
@ -165,6 +166,20 @@ def parse(previous_settings=None):
metavar="FILE", metavar="FILE",
help="yaml file describing where to get openstack sources " help="yaml file describing where to get openstack sources "
"from (default: %default)") "from (default: %default)")
base_group.add_option("--origins-patch",
action="store",
type="string",
dest="origins_patch_fn",
default=None,
metavar="FILE",
help="origins file patch, jsonpath format (rfc6902)")
base_group.add_option("--distros-patch",
action="store",
type="string",
dest="distros_patch_fn",
default=None,
metavar="FILE",
help="distros file patch, jsonpath format (rfc6902)")
base_group.add_option("-j", "--jobs", base_group.add_option("-j", "--jobs",
action="store", action="store",
type="int", type="int",
@ -201,6 +216,14 @@ def parse(previous_settings=None):
default=False, default=False,
help=("when packaging only store /usr directory" help=("when packaging only store /usr directory"
" (default: %default)")) " (default: %default)"))
build_group.add_option("--venv-deploy-dir",
action="store",
type="string",
dest="venv_deploy_dir",
default=None,
help=("for virtualenv builds, make the virtualenv "
"relocatable to a directory different from "
"build directory"))
parser.add_option_group(build_group) parser.add_option_group(build_group)
test_group = OptionGroup(parser, "Test specific options") test_group = OptionGroup(parser, "Test specific options")
@ -227,6 +250,13 @@ def parse(previous_settings=None):
values['origins_fn'] = options.origins_fn values['origins_fn'] = options.origins_fn
values['verbose'] = options.verbose values['verbose'] = options.verbose
values['usr_only'] = options.usr_only values['usr_only'] = options.usr_only
if options.origins_patch_fn:
with open(options.origins_patch_fn) as fp:
values['origins_patch'] = json.load(fp)
if options.distros_patch_fn:
with open(options.distros_patch_fn) as fp:
values['distros_patch'] = json.load(fp)
values['venv_deploy_dir'] = options.venv_deploy_dir
values['prompt_for_passwords'] = options.prompt_for_passwords values['prompt_for_passwords'] = options.prompt_for_passwords
values['show_amount'] = max(0, options.show_amount) values['show_amount'] = max(0, options.show_amount)
values['store_passwords'] = options.store_passwords values['store_passwords'] = options.store_passwords

167
anvil/packaging/venv.py Normal file
View File

@ -0,0 +1,167 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (C) 2014 Yahoo! Inc. 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
# under the License.
import contextlib
import itertools
import os
import tarfile
import six
from anvil import colorizer
from anvil import env
from anvil import log as logging
from anvil import shell as sh
from anvil import utils
from anvil.packaging import base
from anvil.packaging.helpers import pip_helper
LOG = logging.getLogger(__name__)
# TODO(harlowja): think we can remove this...
class VenvInstallHelper(base.InstallHelper):
def pre_install(self, pkg, params=None):
pass
def post_install(self, pkg, params=None):
pass
class VenvDependencyHandler(base.DependencyHandler):
# PBR seems needed everywhere...
_PREQ_PKGS = frozenset(['pbr'])
def __init__(self, distro, root_dir, instances, opts):
super(VenvDependencyHandler, self).__init__(distro, root_dir,
instances, opts)
self.cache_dir = sh.joinpths(self.root_dir, "pip-cache")
def _venv_directory_for(self, instance):
return sh.joinpths(instance.get_option('component_dir'), 'venv')
def _install_into_venv(self, instance, requirements):
venv_dir = self._venv_directory_for(instance)
base_pip = [sh.joinpths(venv_dir, 'bin', 'pip')]
env_overrides = {
'PATH': os.pathsep.join([sh.joinpths(venv_dir, "bin"),
env.get_key('PATH', default_value='')]),
'VIRTUAL_ENV': venv_dir,
}
sh.mkdirslist(self.cache_dir, tracewriter=self.tracewriter)
def try_install(attempt, requirements):
cmd = list(base_pip) + ['install']
cmd.extend([
'--download-cache',
self.cache_dir,
])
if isinstance(requirements, six.string_types):
cmd.extend([
'--requirement',
requirements
])
else:
for req in requirements:
cmd.append(str(req))
sh.execute(cmd, env_overrides=env_overrides)
# Sometimes pip fails downloading things, retry it when this happens...
utils.retry(3, 5, try_install, requirements=requirements)
def _is_buildable(self, instance):
app_dir = instance.get_option('app_dir')
if app_dir and sh.isdir(app_dir) and hasattr(instance, 'egg_info'):
return True
return False
def _replace_deployment_paths(self, root_dirn, pattstr):
for root, _, filenames in os.walk(root_dirn):
for fn in filenames:
cmd = ['sed', '--in-place', pattstr, os.path.join(root, fn)]
sh.execute(cmd=cmd, shell=False)
def package_finish(self):
super(VenvDependencyHandler, self).package_finish()
for instance in self.instances:
if not self._is_buildable(instance):
continue
venv_dir = sh.abspth(self._venv_directory_for(instance))
# Replace paths with virtualenv deployment directory.
if self.opts.get('venv_deploy_dir'):
deploy_dir = os.path.join(self.opts.get('venv_deploy_dir'),
instance.name)
pattstr = ('s#{searchstr}#{replacestr}#g'.format(
searchstr=instance.get_option('component_dir'),
replacestr=deploy_dir))
bin_dir = os.path.join(venv_dir, 'bin')
self._replace_deployment_paths(bin_dir, pattstr)
# Create a tarball containing the virtualenv.
tar_filename = sh.joinpths(venv_dir, '%s-venv.tar.gz' % instance.name)
LOG.info("Making tarball of %s built for %s at %s", venv_dir,
instance.name, tar_filename)
with contextlib.closing(tarfile.open(tar_filename, "w:gz")) as tfh:
for path in sh.listdir(venv_dir, recursive=True):
tfh.add(path, recursive=False, arcname=path[len(venv_dir):])
def package_start(self):
super(VenvDependencyHandler, self).package_start()
for instance in self.instances:
if not self._is_buildable(instance):
continue
# Create a virtualenv...
venv_dir = self._venv_directory_for(instance)
sh.mkdirslist(venv_dir, tracewriter=self.tracewriter)
cmd = ['virtualenv', '--clear', venv_dir]
LOG.info("Creating virtualenv at %s", colorizer.quote(venv_dir))
sh.execute(cmd)
if self._PREQ_PKGS:
self._install_into_venv(instance, self._PREQ_PKGS)
def package_instance(self, instance):
# Skip things that aren't python...
if self._is_buildable(instance):
requires_what = self._filter_download_requires()
requires_keys = set()
for req in requires_what:
if isinstance(req, six.string_types):
req = pip_helper.extract_requirement(req)
requires_keys.add(req.key)
egg_info = getattr(instance, 'egg_info', None)
if egg_info is not None:
# Ensure we have gotten all the things...
test_dependencies = (egg_info.get('test_dependencies', [])
if instance.get_bool_option(
'use_tests_requires', default_value=True)
else [])
for req in itertools.chain(egg_info.get('dependencies', []),
test_dependencies):
if isinstance(req, six.string_types):
req = pip_helper.extract_requirement(req)
if req.key not in requires_keys:
requires_what.append(req)
requires_keys.add(req.key)
self._install_into_venv(instance, requires_what)
self._install_into_venv(instance, [instance.get_option('app_dir')])
else:
LOG.warn("Skipping building %s (not python)",
colorizer.quote(instance.name, quote_color='red'))
def download_dependencies(self):
pass

View File

@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import jsonpatch
import six import six
from anvil import colorizer from anvil import colorizer
@ -33,9 +35,13 @@ class Persona(object):
self.component_options = kargs.get('options') or {} self.component_options = kargs.get('options') or {}
self.no_origins = kargs.get('no-origin') or [] self.no_origins = kargs.get('no-origin') or []
def match(self, distros, origins_fn): def match(self, distros, origins_fn, origins_patch=None):
# Filter out components that are disabled in origins file # Filter out components that are disabled in origins file
origins = utils.load_yaml(origins_fn) origins = utils.load_yaml(origins_fn)
# Apply any user specified patches to origins file
if origins_patch:
patch = jsonpatch.JsonPatch(origins_patch)
patch.apply(origins, in_place=True)
for c in self.wanted_components: for c in self.wanted_components:
if c not in origins: if c not in origins:
if c in self.no_origins: if c in self.no_origins:

View File

@ -122,6 +122,7 @@ def execute(cmd,
if env_overrides and len(env_overrides): if env_overrides and len(env_overrides):
process_env = env.get() process_env = env.get()
for k, v in env_overrides.items(): for k, v in env_overrides.items():
LOG.debug("Using environment override '%s' => '%s'", k, v)
process_env[k] = str(v) process_env[k] = str(v)
# Run command process. # Run command process.

View File

@ -121,8 +121,7 @@ def expand_template(contents, params):
tpl = Template(source=str(contents), tpl = Template(source=str(contents),
searchList=[params], searchList=[params],
compilerSettings={ compilerSettings={
'useErrorCatcher': True, 'useErrorCatcher': True})
})
return tpl.respond() return tpl.respond()

View File

@ -0,0 +1,12 @@
[
{
"path": "/install_helper",
"value": "anvil.packaging.venv:VenvInstallHelper",
"op": "replace"
},
{
"path": "/dependency_handler",
"value": { "name": "anvil.packaging.venv:VenvDependencyHandler" },
"op": "replace"
}
]

View File

@ -0,0 +1,27 @@
[
{ "path": "/ceilometer/use_tests_requires", "value": false, "op": "add" },
{ "path": "/ceilometer-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/cinder-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/cinder/use_tests_requires", "value": false, "op": "add" },
{ "path": "/django-openstack-auth/use_tests_requires", "value": false, "op": "add" },
{ "path": "/glance-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/glance/use_tests_requires", "value": false, "op": "add" },
{ "path": "/heat/use_tests_requires", "value": false, "op": "add" },
{ "path": "/heat-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/horizon/use_tests_requires", "value": false, "op": "add" },
{ "path": "/keystone-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/keystone/use_tests_requires", "value": false, "op": "add" },
{ "path": "/nova-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/nova/use_tests_requires", "value": false, "op": "add" },
{ "path": "/novnc/use_tests_requires", "value": false, "op": "add" },
{ "path": "/openstack-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/oslo-config/use_tests_requires", "value": false, "op": "add" },
{ "path": "/oslo-incubator/use_tests_requires", "value": false, "op": "add" },
{ "path": "/neutron-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/neutron/use_tests_requires", "value": false, "op": "add" },
{ "path": "/swift-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/trove-client/use_tests_requires", "value": false, "op": "add" },
{ "path": "/trove/use_tests_requires", "value": false, "op": "add" },
{ "path": "/oslo-messaging/use_tests_requires", "value": false, "op": "add" },
{ "path": "/pycadf/use_tests_requires", "value": false, "op": "add" }
]

View File

@ -10,3 +10,4 @@ PyYAML>=3.1.0
six>=1.4.1 six>=1.4.1
termcolor termcolor
argparse argparse
jsonpatch>=1.1