instack/instack/runner.py

224 lines
7.9 KiB
Python

# Copyright 2013, Red Hat 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.
from distutils import dir_util
import logging
import os
import shutil
import subprocess
import sys
import tempfile
from diskimage_builder import element_dependencies
from instack import element
# dib-run-parts has moved in dibv2 to an internal call. We don't
# really want to introduce a dependency on dib-utils here. For v1
# just call it from path.
# Note this is fragile as dib considers this internal
_DIB_RUN_PARTS = 'dib-run-parts'
try:
import diskimage_builder.paths
_DIB_RUN_PARTS = os.path.join(diskimage_builder.paths.get_path('lib'),
'dib-run-parts')
except ImportError:
pass
LOG = logging.getLogger()
class ElementRunner(object):
def __init__(self, elements, hooks, element_paths=None, blacklist=None,
exclude_elements=None, dry_run=False, interactive=False,
no_cleanup=False):
"""Element Runner initialization.
:param elements: Element names to apply.
:type elements: list.
:param hooks: Hooks to run for each element.
:type hooks: list.
:param element_paths: File system paths to search for elements.
:type element_paths: list of strings.
:param dry_run: If True, do not actually run the hooks.
:type dry_run: bool
"""
self.elements = elements
self.dry_run = dry_run
self.hooks = hooks
self.blacklist = blacklist or []
self.exclude_elements = exclude_elements or []
self.interactive = interactive
self.no_cleanup = no_cleanup
self.loaded_elements = {}
self.tmp_hook_dir = tempfile.mkdtemp()
self.environment_file = os.path.join(self.tmp_hook_dir,
'environment.d',
'00-dib-v2-env')
# the environment variable should override anything passed in
if 'ELEMENTS_PATH' in os.environ:
self.element_paths = os.environ['ELEMENTS_PATH'].split(':')
else:
self.element_paths = element_paths
if self.element_paths is None:
raise Exception("No element paths specified")
LOG.info('Initialized with elements path: %s' %
' '.join(list(self.element_paths)))
self.load_elements()
self.load_dependencies()
self.process_exclude_elements()
self.copy_elements()
self.generate_environment()
def generate_environment(self):
"""Generate a dib v2 environment."""
# NOTE(bnemec): Older versions of dib don't need this. We can tell
# by looking for the --env parameter to element-info.
check_output = subprocess.check_output(['element-info', '-h'])
if '--env' not in check_output:
return
command = ['element-info', '--env'] + list(self.elements)
env_output = subprocess.check_output(command)
with open(self.environment_file, 'w') as f:
f.write(env_output)
def run(self):
"""Apply the elements by running each specified hook."""
for hook in self.hooks:
self.run_hook(hook)
self.cleanup()
def cleanup(self):
"""Clean up after a run."""
if not self.no_cleanup:
shutil.rmtree(self.tmp_hook_dir)
def load_elements(self):
"""Load all elements from self.element_paths.
This populates self.loaded_elements.
"""
for path in self.element_paths:
self.process_path(path)
def copy_elements(self):
"""Copy elements to apply to a temporary directory."""
# self.tmp_hook_dir may exist from a previous run, delete it if so.
if os.path.exists(self.tmp_hook_dir):
shutil.rmtree(self.tmp_hook_dir)
os.makedirs(self.tmp_hook_dir)
for elem in self.elements:
element_dir = self.loaded_elements[elem].directory
dir_util.copy_tree(element_dir, self.tmp_hook_dir)
# elements expect this environment variable to be set
os.environ['TMP_HOOKS_PATH'] = self.tmp_hook_dir
tmp_path = '%s/bin' % self.tmp_hook_dir
if 'PATH' in os.environ:
tmp_path = os.environ["PATH"] + os.pathsep + tmp_path
os.environ["PATH"] = tmp_path
def process_path(self, path):
"""Load elements from a given filesystem path.
:param path: Filesystem path from which to load elements.
:type path: str.
"""
if not os.access(path, os.R_OK):
raise Exception("Can't read from elements path at %s." % path)
for elem in os.listdir(path):
if not os.path.isdir(os.path.join(path, elem)):
continue
self.loaded_elements[elem] = element.Element(
os.path.join(path, elem))
def load_dependencies(self):
"""Load and add all element dependencies to self.elements."""
all_elements = element_dependencies.expand_dependencies(
self.elements, ':'.join(self.element_paths))
self.elements = all_elements
os.environ['IMAGE_ELEMENT'] = ' '.join(
[x for x in sorted(self.elements)])
LOG.info("List of all elements and dependencies: %s" %
' '.join(list(self.elements)))
def process_exclude_elements(self):
"""Remove any elements that have been specified as excluded."""
for elem in self.exclude_elements:
if elem in self.elements:
LOG.info("Excluding element %s" % elem)
self.elements.remove(elem)
# Need to redefine OS.environ['IMAGE_ELEMENT'] after removing excludes
os.environ['IMAGE_ELEMENT'] = ' '.join(
[x for x in sorted(self.elements)])
LOG.info("List of all elements and dependencies after excludes: %s" %
' '.join(list(self.elements)))
def run_hook(self, hook):
"""Run a hook on the current system.
:param hook: name of hook to run
:type hook: str
"""
LOG.info(" Running hook %s" % hook)
hook_dir = os.path.join(self.tmp_hook_dir, '%s.d' % hook)
if not os.path.exists(hook_dir):
LOG.info(" Skipping hook %s, the hook directory doesn't "
"exist at %s" % (hook, hook_dir))
return
for blacklisted_script in self.blacklist:
if blacklisted_script in os.listdir(hook_dir):
LOG.debug(" Blacklisting %s" % blacklisted_script)
os.unlink(os.path.join(hook_dir, blacklisted_script))
command = [_DIB_RUN_PARTS, hook_dir]
if self.dry_run:
LOG.info(" Dry Run specified, not running hook")
else:
rc = call(command, env=os.environ)
if rc != 0:
LOG.error(" Hook FAILED.")
raise Exception("Failed running command %s" % command)
def call(command, **kwargs):
"""Call out to run a command via subprocess."""
LOG.debug(' executing command: %s' % command)
LOG.info('############### Begin stdout/stderr logging ###############')
rc = subprocess.call(command,
stdout=sys.stdout,
stderr=sys.stderr,
**kwargs)
LOG.info('############### End stdout/stderr logging ###############')
LOG.debug(' exited with code: %s' % rc)
return rc