anvil/anvil/distro.py

215 lines
8.0 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
# Copyright (C) 2012 New Dream Network, LLC (DreamHost) 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 collections
import copy
import glob
import jsonpatch
import os
import platform
import re
import shlex
import six
from anvil import exceptions as excp
from anvil import importer
from anvil import log as logging
from anvil import pprint
from anvil import shell as sh
from anvil import utils
LOG = logging.getLogger(__name__)
Component = collections.namedtuple( # pylint: disable=C0103
"Component", 'entry_point,options,siblings')
class Distro(object):
def __init__(self,
name, platform_pattern,
install_helper, dependency_handler,
components, **kwargs):
self.name = name
self._platform_pattern_text = platform_pattern
self._platform_pattern = re.compile(platform_pattern, re.IGNORECASE)
self._install_helper = install_helper
self._dependency_handler = dependency_handler
self._commands = kwargs.get('commands', {})
self._components = components
self.inject_platform_overrides(kwargs)
def inject_platform_overrides(self, potential_data, source='??'):
if 'platform_overrides' not in potential_data:
return
overrides = potential_data['platform_overrides']
plts = _get_platform_names()
patterns = [(k, re.compile(k, re.IGNORECASE), override)
for k, override in six.iteritems(overrides)]
for k, pat, override in patterns:
if any(pat.search(plt) for plt in plts):
LOG.info("Merging in 'platform_overrides' that matched"
" platform %s from %s (sub-key %s)", plts, source, k)
self._dependency_handler = utils.recursive_merge(
self._dependency_handler, override)
def pformat(self, item_max_len=None):
data = {
'name': self.name,
'dependency_handler': self._dependency_handler,
'commands': self._commands,
'pattern': "/%s/i" % self._platform_pattern_text,
}
return pprint.pformat(data, item_max_len=item_max_len)
def _fetch_value(self, root, keys, quiet):
end_key = keys[-1]
for k in keys[0:-1]:
if quiet:
root = root.get(k)
if root is None:
return None
else:
root = root[k]
end_value = None
if not quiet:
end_value = root[end_key]
else:
end_value = root.get(end_key)
return end_value
def get_dependency_config(self, key, *more_keys, **kwargs):
root = dict(self._dependency_handler)
# NOTE(harlowja): Don't allow access to the dependency handler class
# name. Access should be via the property instead.
root.pop('name', None)
keys = [key] + list(more_keys)
return self._fetch_value(root, keys, kwargs.get('quiet', False))
def get_command_config(self, key, *more_keys, **kwargs):
root = dict(self._commands)
keys = [key] + list(more_keys)
return self._fetch_value(root, keys, kwargs.get('quiet', False))
def get_command(self, key, *more_keys, **kwargs):
"""Retrieves a string for running a command from the setup
and splits it to return a list.
"""
val = self.get_command_config(key, *more_keys, **kwargs)
if not val:
return []
else:
return shlex.split(val)
def known_component(self, name):
return name in self._components
def supports_platform(self, platform_name):
"""Does this distro support the named platform?
:param platform_name: Return value from platform.platform().
"""
return bool(self._platform_pattern.search(platform_name))
@property
def install_helper_class(self):
"""Return an install helper that will work for this distro."""
return importer.import_entry_point(self._install_helper)
@property
def dependency_handler_class(self):
"""Return a dependency handler that will work for this distro."""
return importer.import_entry_point(self._dependency_handler["name"])
def extract_component(self, name, action, default_entry_point_creator=None):
"""Return the class + component info to use for doing the action w/the component."""
try:
# Use a copy instead of the original since we will be
# modifying this dictionary which may not be wanted for future
# usages of this dictionary (so keep the original clean)...
component_info = copy.deepcopy(self._components[name])
except KeyError:
component_info = {}
action_classes = component_info.pop('action_classes', {})
if default_entry_point_creator is not None:
default_action_classes = default_entry_point_creator(name,
copy.deepcopy(component_info))
if default_action_classes:
for (an_action, entry_point) in six.iteritems(default_action_classes):
if an_action not in action_classes:
action_classes[an_action] = entry_point
try:
entry_point = action_classes.pop(action)
except KeyError:
raise RuntimeError('No entrypoint configured/generated for'
' %r %r for distribution %r' % (action, name, self.name))
else:
return Component(entry_point, component_info, action_classes)
def _get_platform_names():
plts = [
platform.platform(),
]
linux_plt = platform.linux_distribution()[0:2]
linux_plt = "-".join(linux_plt)
linux_plt = linux_plt.replace(" ", "-")
plts.append(linux_plt)
return [plt.lower() for plt in plts]
def _match_distros(distros):
plts = _get_platform_names()
matches = []
for d in distros:
if any(d.supports_platform(plt) for plt in plts):
matches.append(d)
if not matches:
raise excp.ConfigException('No distro matched for platform %s' % plts)
else:
return matches
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 = []
patch = jsonpatch.JsonPatch(distros_patch) if distros_patch else None
input_files = glob.glob(sh.joinpths(path, '*.yaml'))
if not input_files:
raise excp.ConfigException('Did not find any distro definition files in %r' % path)
for fn in input_files:
LOG.debug("Attempting to load distro definition from %r", fn)
try:
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:
LOG.warning('Could not load distro definition from %r: %s', fn, err)
else:
if 'name' not in cls_kvs:
name, _ext = os.path.splitext(sh.basename(fn))
cls_kvs['name'] = name
distro_possibles.append(Distro(**cls_kvs))
matches = _match_distros(distro_possibles)
LOG.debug("Matched distros %s", [m.name for m in matches])
return matches