a32a8f1ce1
The current check only validates that an element that specifies "element-provides" doesn't conflict with a "real" element. We also want to check this against the provides of other elements. A real example is with a "block-device" element. There is no actual "block-device" element; we can have multiple elements provide it (block-device-[gpt,mbr,efi], say) but we only want one of them at a time. Update the unit test for this. Change-Id: I59d4aa5f6f09e2892b213e154befa10d85e95ca3
353 lines
12 KiB
Python
353 lines
12 KiB
Python
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# 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 __future__ import print_function
|
|
import argparse
|
|
import collections
|
|
import errno
|
|
import logging
|
|
import os
|
|
import sys
|
|
import yaml
|
|
|
|
import diskimage_builder.logging_config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MissingElementException(Exception):
|
|
pass
|
|
|
|
|
|
class AlreadyProvidedException(Exception):
|
|
pass
|
|
|
|
|
|
class MissingOSException(Exception):
|
|
pass
|
|
|
|
|
|
class InvalidElementDir(Exception):
|
|
pass
|
|
|
|
|
|
class Element(object):
|
|
"""An element"""
|
|
|
|
def _get_element_set(self, path):
|
|
"""Get element set from element-[deps|provides] file
|
|
|
|
Arguments:
|
|
:param path: path to element description
|
|
|
|
:return: the set of elements in the file, or a blank set if
|
|
the file is not found.
|
|
"""
|
|
try:
|
|
with open(path) as f:
|
|
lines = (line.strip() for line in f)
|
|
# Strip blanks, but do we want to strip comment lines
|
|
# too? No use case at the moment, and comments might
|
|
# break other things that poke at the element-* files.
|
|
lines = (line for line in lines if line)
|
|
return set(lines)
|
|
except IOError as e:
|
|
if e.errno == errno.ENOENT:
|
|
return set([])
|
|
else:
|
|
raise
|
|
|
|
def _make_rdeps(self, all_elements):
|
|
"""Make a list of reverse dependencies (who depends on us).
|
|
|
|
Only valid after _find_all_elements()
|
|
|
|
Arguments:
|
|
:param all_elements: dict as returned by _find_all_elements()
|
|
|
|
:return: nothing, but elements will have r_depends var
|
|
"""
|
|
|
|
# note; deliberatly left out of __init__ so that accidental
|
|
# access without init raises error
|
|
self.r_depends = []
|
|
for name, element in all_elements.items():
|
|
if self.name in element.depends:
|
|
self.r_depends.append(element.name)
|
|
|
|
def __init__(self, name, path):
|
|
"""A new element
|
|
|
|
:param name: The element name
|
|
:param path: Full path to element. element-deps and
|
|
element-provides files will be parsed
|
|
"""
|
|
self.name = name
|
|
self.path = path
|
|
|
|
# read the provides & depends files for this element into a
|
|
# set; if the element has them.
|
|
self.provides = self._get_element_set(
|
|
os.path.join(path, 'element-provides'))
|
|
self.depends = self._get_element_set(
|
|
os.path.join(path, 'element-deps'))
|
|
|
|
logger.debug("New element : %s", str(self))
|
|
|
|
def __eq__(self, other):
|
|
return self.name == other.name
|
|
|
|
def __repr__(self):
|
|
return self.name
|
|
|
|
def __str__(self):
|
|
return '%s p:<%s> d:<%s>' % (self.name,
|
|
','.join(self.provides),
|
|
','.join(self.depends))
|
|
|
|
|
|
def _get_elements_dir():
|
|
if not os.environ.get('ELEMENTS_PATH'):
|
|
raise Exception("$ELEMENTS_PATH must be set.")
|
|
return os.environ['ELEMENTS_PATH']
|
|
|
|
|
|
def _expand_element_dependencies(user_elements, all_elements):
|
|
"""Expand user requested elements using element-deps files.
|
|
|
|
Arguments:
|
|
:param user_elements: iterable enumerating the elements a user requested
|
|
:param all_elements: Element object dictionary from find_all_elements
|
|
|
|
:return: a set containing the names of user_elements and all
|
|
dependent elements including any transitive dependencies.
|
|
"""
|
|
final_elements = set(user_elements)
|
|
check_queue = collections.deque(user_elements)
|
|
provided = set()
|
|
provided_by = collections.defaultdict(list)
|
|
|
|
while check_queue:
|
|
# bug #1303911 - run through the provided elements first to avoid
|
|
# adding unwanted dependencies and looking for virtual elements
|
|
element = check_queue.popleft()
|
|
if element in provided:
|
|
continue
|
|
elif element not in all_elements:
|
|
raise MissingElementException("Element '%s' not found" % element)
|
|
|
|
element_obj = all_elements[element]
|
|
|
|
element_deps = element_obj.depends
|
|
element_provides = element_obj.provides
|
|
# Check that we are not providing an element which has already
|
|
# been provided by someone else, and additionally save which
|
|
# elements provide another element
|
|
for provide in element_provides:
|
|
if provide in provided:
|
|
raise AlreadyProvidedException(
|
|
"%s: already provided by %s" %
|
|
(provide, provided_by[provide]))
|
|
provided_by[provide].append(element)
|
|
provided.update(element_provides)
|
|
check_queue.extend(element_deps - (final_elements | provided))
|
|
final_elements.update(element_deps)
|
|
|
|
conflicts = set(user_elements) & provided
|
|
if conflicts:
|
|
logger.error(
|
|
"The following elements are already provided by another element")
|
|
for element in conflicts:
|
|
logger.error("%s : already provided by %s",
|
|
element, provided_by[element])
|
|
raise AlreadyProvidedException()
|
|
|
|
if "operating-system" not in provided:
|
|
raise MissingOSException("Please include an operating system element")
|
|
|
|
out = final_elements - provided
|
|
return([all_elements[element] for element in out])
|
|
|
|
|
|
def _find_all_elements(paths=None):
|
|
"""Build a dictionary Element() objects
|
|
|
|
Walk ELEMENTS_PATH and find all elements. Make an Element object
|
|
for each element we wish to consider. Note we process overrides
|
|
such that elements specified earlier in the ELEMENTS_PATH override
|
|
those seen later.
|
|
|
|
:param paths: A list of paths to find elements in. If None will
|
|
use ELEMENTS_PATH from environment
|
|
|
|
:return: a dictionary of all elements
|
|
"""
|
|
|
|
all_elements = {}
|
|
|
|
# note we process the later entries *first*, so that earlier
|
|
# entries will override later ones. i.e. with
|
|
# ELEMENTS_PATH=path1:path2:path3
|
|
# we want the elements in "path1" to override "path3"
|
|
if not paths:
|
|
paths = list(reversed(_get_elements_dir().split(':')))
|
|
else:
|
|
paths = list(reversed(paths.split(':')))
|
|
|
|
logger.debug("ELEMENTS_PATH is: %s", ":".join(paths))
|
|
|
|
for path in paths:
|
|
if not os.path.isdir(path):
|
|
raise InvalidElementDir("ELEMENTS_PATH entry '%s' "
|
|
"is not a directory " % path)
|
|
|
|
# In words : make a list of directories in "path". Since an
|
|
# element is a directory, this is our list of elements.
|
|
elements = [os.path.realpath(os.path.join(path, f))
|
|
for f in os.listdir(path)
|
|
if os.path.isdir(os.path.join(path, f))]
|
|
|
|
for element in elements:
|
|
# the element name is the last part of the full path in
|
|
# element (these are all directories, we know that from
|
|
# above)
|
|
name = os.path.basename(element)
|
|
|
|
new_element = Element(name, element)
|
|
if name in all_elements:
|
|
logger.warning("Element <%s> overrides <%s>",
|
|
new_element.path, all_elements[name].path)
|
|
|
|
all_elements[name] = new_element
|
|
|
|
# Now we have all the elements, make a call on each element to
|
|
# store it's reverse dependencies
|
|
for name, element in all_elements.items():
|
|
element._make_rdeps(all_elements)
|
|
|
|
return all_elements
|
|
|
|
|
|
def _get_elements(elements, paths=None):
|
|
"""Return the canonical list of Element objects
|
|
|
|
This function returns Element objects. For exernal calls, use
|
|
get_elements which returns a simple tuple & list.
|
|
|
|
:param elements: user specified list of elements
|
|
:param paths: element paths, default to environment
|
|
|
|
"""
|
|
all_elements = _find_all_elements(paths)
|
|
return _expand_element_dependencies(elements, all_elements)
|
|
|
|
|
|
def get_elements(elements, paths=None):
|
|
"""Return the canonical list of elements with their dependencies
|
|
|
|
.. note::
|
|
|
|
You probably do not want to use this! Elements that require
|
|
access to the list of all other elements should generally use
|
|
the environment variables exported by disk-image-create below.
|
|
|
|
:param elements: user specified elements
|
|
:param paths: Alternative ELEMENTS_PATH; default is to use from env
|
|
|
|
:return: A de-duplicated list of tuples [(element, path),
|
|
(element, path) ...] with all elements and their
|
|
dependents, including any transitive dependencies.
|
|
"""
|
|
|
|
elements = _get_elements(elements, paths)
|
|
return [(element.name, element.path) for element in elements]
|
|
|
|
|
|
def expand_dependencies(user_elements, element_dirs):
|
|
"""Deprecated method for expanding element dependencies.
|
|
|
|
.. warning::
|
|
|
|
DO NOT USE THIS FUNCTION. For compatibility reasons, this
|
|
function does not provide paths to the returned elements. This
|
|
means the caller must process override rules if two elements
|
|
with the same name appear in element_dirs
|
|
|
|
:param user_elements: iterable enumerating the elements a user requested
|
|
:param element_dirs: The ELEMENTS_PATH to process
|
|
|
|
:return: a set containing user_elements and all dependent
|
|
elements including any transitive dependencies.
|
|
"""
|
|
logger.warning("expand_dependencies() deprecated, use get_elements")
|
|
elements = _get_elements(user_elements, element_dirs)
|
|
return set([element.name for element in elements])
|
|
|
|
|
|
def _output_env_vars(elements):
|
|
"""Output eval-able bash strings for IMAGE_ELEMENT vars
|
|
|
|
:param elements: list of Element objects to represent
|
|
"""
|
|
# first the "legacy" environment variable that just lists the
|
|
# elements
|
|
print("export IMAGE_ELEMENT='%s'" %
|
|
' '.join([element.name for element in elements]))
|
|
|
|
# Then YAML
|
|
output = {}
|
|
for element in elements:
|
|
output[element.name] = element.path
|
|
print("export IMAGE_ELEMENT_YAML='%s'" % yaml.safe_dump(output))
|
|
|
|
# Then bash array. Unfortunately, bash can't export array
|
|
# variables. So we take a compromise and produce an exported
|
|
# function that outputs the string to re-create the array.
|
|
# You can then simply do
|
|
# eval declare -A element_array=$(get_image_element_array)
|
|
# and you have it.
|
|
output = ""
|
|
for element in elements:
|
|
output += '[%s]=%s ' % (element.name, element.path)
|
|
print("function get_image_element_array {\n"
|
|
" echo \"%s\"\n"
|
|
"};\n"
|
|
"export -f get_image_element_array;" % output)
|
|
|
|
|
|
def main():
|
|
diskimage_builder.logging_config.setup()
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('elements', nargs='+',
|
|
help='display dependencies of the given elements')
|
|
parser.add_argument('--env', '-e', action='store_true',
|
|
default=False,
|
|
help=('Output eval-able bash strings for '
|
|
'IMAGE_ELEMENT variables'))
|
|
|
|
args = parser.parse_args(sys.argv[1:])
|
|
|
|
elements = _get_elements(args.elements)
|
|
|
|
if args.env:
|
|
_output_env_vars(elements)
|
|
else:
|
|
# deprecated compatibility output; doesn't include paths.
|
|
print(' '.join([element.name for element in elements]))
|
|
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
main()
|