1933N/A#!/usr/bin/python
1933N/A#
1933N/A# CDDL HEADER START
1933N/A#
1933N/A# The contents of this file are subject to the terms of the
1933N/A# Common Development and Distribution License (the "License").
1933N/A# You may not use this file except in compliance with the License.
1933N/A#
1933N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
1933N/A# or http://www.opensolaris.org/os/licensing.
1933N/A# See the License for the specific language governing permissions
1933N/A# and limitations under the License.
1933N/A#
1933N/A# When distributing Covered Code, include this CDDL HEADER in each
1933N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
1933N/A# If applicable, add the following below this CDDL HEADER, with the
1933N/A# fields enclosed by brackets "[]" replaced with your own identifying
1933N/A# information: Portions Copyright [yyyy] [name of copyright owner]
1933N/A#
1933N/A# CDDL HEADER END
1933N/A#
1933N/A
1933N/A#
3356N/A# Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
1933N/A#
1933N/A
1933N/Aimport os.path
3234N/Aimport six
1933N/Aimport xml.dom.minidom as minidom
1933N/Aimport xml.parsers
1933N/Aimport xml.parsers.expat
1933N/A
1933N/Aimport pkg.flavor.base as base
1933N/Afrom pkg.portable import PD_LOCAL_PATH, PD_PROTO_DIR, PD_PROTO_DIR_LIST
1933N/A
1933N/A# A list of locations beneath a given proto_dir where we expect to
1933N/A# find SMF manifest files
1933N/Amanifest_locations = [ "lib/svc/manifest", "var/svc/manifest" ]
1933N/A
1933N/Aclass SMFManifestDependency(base.PublishingDependency):
1933N/A
1933N/A # maps SMF fmris to the manifest files that defined them
1933N/A instance_mf = None
1933N/A
1933N/A # maps SMF FMRIs to a list the SMF FMRIs they declare as dependencies
1933N/A instance_deps = None
1933N/A
1933N/A manifest = None
1933N/A
1933N/A def __init__(self, action, path, pkg_vars, proto_dir):
1933N/A """ See __init__ for PublishingDependency.
1933N/A """
1933N/A self.manifest = path
2682N/A full_paths = None
3234N/A if isinstance(path, six.string_types):
2682N/A base_names = [os.path.basename(path)]
2682N/A paths = [os.path.dirname(path)]
2682N/A
2682N/A elif isinstance(path, tuple):
2682N/A # we can depend on multiple files delivering instances
2682N/A # of a service that was marked as a dependency. If we
2682N/A # do, we must specify the full paths to the manifests
2682N/A # and not a basenames/paths pair, since SMF does not
2682N/A # search for SMF manifests this way.
2682N/A base_names = []
2682N/A paths = []
2682N/A full_paths = [p.replace(proto_dir, "").lstrip("/")
2682N/A for p in path]
2682N/A else:
2682N/A raise base.InvalidPublishingDependency("A string or "
2682N/A "tuple must be specified for 'path'.")
2682N/A
1933N/A base.PublishingDependency.__init__(self, action,
2682N/A base_names, paths, pkg_vars, proto_dir, "smf_manifest",
2682N/A full_paths=full_paths)
1933N/A
2501N/A def __repr__(self):
3158N/A return "SMFDep({0}, {1}, {2}, {3})".format(self.action,
2682N/A self.base_names, self.run_paths, self.pkg_vars)
2501N/A
2501N/A @staticmethod
2501N/A def _clear_cache():
2501N/A """Clear our manifest caches. This is primarily provided for
2501N/A test code."""
2501N/A SMFManifestDependency.instance_mf = None
2501N/A SMFManifestDependency.instance_deps = None
2501N/A
1933N/A @staticmethod
1933N/A def populate_cache(proto_dirs, force_update=False):
1933N/A """Build our instance_mf and instance_deps dictionaries
1933N/A from the known SMF manifests installed on the current system
1933N/A and those that appear in the proto_dirs.
1933N/A """
1933N/A
1933N/A if not force_update:
1933N/A if SMFManifestDependency.instance_mf != None:
1933N/A return
1933N/A
1933N/A SMFManifestDependency.instance_mf = {}
1933N/A SMFManifestDependency.instance_deps = {}
1933N/A
1933N/A manifest_paths = []
1933N/A
1933N/A # we want our proto_dirs to be the authoritative source
1933N/A # for SMF manifests, so scan the local system first, then
1933N/A # iterate through the proto_dirs, starting from the
1933N/A # oldest, overwriting with progressively newer proto_dirs
1933N/A for location in manifest_locations:
1933N/A manifest_paths.append(os.path.join("/", location))
1933N/A for proto_dir in reversed(proto_dirs):
1933N/A for location in manifest_locations:
1933N/A manifest_paths.append(os.path.join(proto_dir,
1933N/A location))
1933N/A
1933N/A for location in manifest_paths:
3194N/A for dirpath, dirnames, filenames in os.walk(location):
3194N/A for f in filenames:
3194N/A manifest_file = os.path.join(
3194N/A dirpath, f)
3194N/A SMFManifestDependency.__populate_smf_dics(
3194N/A manifest_file)
1933N/A @staticmethod
1933N/A def __populate_smf_dics(manifest_file):
1933N/A """Add a information information about the SMF instances and
1933N/A their dependencies from the given manifest_file to a global
1933N/A set of dictionaries."""
1933N/A
1933N/A instance_mf, instance_deps = parse_smf_manifest(
1933N/A manifest_file)
1933N/A
1933N/A # more work is needed here when
1933N/A # multiple-manifests per service is supported by
1933N/A # SMF: we'll need to merge additional
1933N/A # service-level dependencies into each service
1933N/A # instance as each manifest gets added.
1933N/A if instance_mf:
1933N/A SMFManifestDependency.instance_mf.update(
1933N/A instance_mf)
1933N/A if instance_deps:
1933N/A SMFManifestDependency.instance_deps.update(
1933N/A instance_deps)
1933N/A
1933N/Adef split_smf_fmri(fmri):
1933N/A """Split an SMF FMRI into constituent parts, returning the svc protocol,
1933N/A the service name, and the instance name, if any."""
1933N/A
1933N/A protocol = None
1933N/A service = None
1933N/A instance = None
1933N/A
1933N/A arr = fmri.split(":")
1933N/A if len(arr) == 2 and arr[0] == "svc":
1933N/A protocol = "svc"
1933N/A service = arr[1]
1933N/A elif len(arr) == 3 and arr[0] == "svc":
1933N/A protocol = "svc"
1933N/A service = arr[1]
1933N/A instance = arr[2]
1933N/A else:
1933N/A raise ValueError(_("FMRI does not appear to be valid"))
1933N/A return protocol, service, instance
1933N/A
1933N/Adef search_smf_dic(fmri, dictionary):
1933N/A """Search a dictionary of SMF FMRI mappings, returning a list of
1933N/A results. If the FMRI points to an instance, we can return quickly. If
1933N/A the FMRI points to a service, we return all matching instances. Note
1933N/A if the dictionary contains service FMRIs, those won't appear in the
1933N/A results - we only ever return instances."""
1933N/A
1933N/A protocol, service, instance = split_smf_fmri(fmri)
1933N/A results = []
1933N/A if instance is not None:
1933N/A if fmri in dictionary:
1933N/A results.append(dictionary[fmri])
1933N/A else:
1933N/A # return all matching instances of this service
1933N/A for item in dictionary:
1933N/A if item.startswith(protocol + ":" + service + ":"):
1933N/A results.append(dictionary[item])
1933N/A return results
1933N/A
1933N/Adef get_smf_dependencies(fmri, instance_deps):
1933N/A """Given an instance FMRI, determine the FMRIs it depends on. If we
1933N/A match more than one fmri, we raise an exception. """
1933N/A
1933N/A results = search_smf_dic(fmri, instance_deps)
1933N/A if len(results) == 1:
1933N/A return results[0]
1933N/A elif len(results) > 1:
1933N/A # this can only happen if we've been asked to resolve a
1933N/A # service-level FMRI, not a fully qualified instance FMRI
1933N/A raise ValueError(
3158N/A _("more than one set of dependencies found: {0}").format(
3158N/A results))
1933N/A
1933N/A results = search_smf_dic(fmri, SMFManifestDependency.instance_deps)
1933N/A if len(results) == 1:
1933N/A return results[0]
1933N/A elif len(results) > 1:
1933N/A raise ValueError(
3158N/A _("more than one set of dependencies found: {0}").format(
3158N/A results))
1933N/A
1933N/A return []
1933N/A
1933N/Adef resolve_smf_dependency(fmri, instance_mf):
1933N/A """Given an SMF FMRI that satisfies a given SMF dependency, determine
1933N/A which file(s) deliver that dependency using both the provided
1933N/A instance_mf dictionary and the global SmfManifestDependency dictionary.
1933N/A If multiple files match, we have a problem."""
1933N/A
1933N/A manifests = set()
1933N/A
1933N/A manifests.update(search_smf_dic(fmri, instance_mf))
1933N/A
1933N/A manifests.update(search_smf_dic(
1933N/A fmri, SMFManifestDependency.instance_mf))
1933N/A
1933N/A if len(manifests) == 0:
1933N/A # we can't satisfy the dependency at all
1933N/A raise ValueError(_("cannot resolve FMRI to a delivered file"))
1933N/A
2682N/A return list(manifests)
1933N/A
1933N/Adef process_smf_manifest_deps(action, pkg_vars, **kwargs):
1933N/A """Given an action and a place to find the file it references, if the
1933N/A file is an SMF manifest, we return a list of SmfManifestDependencies
1933N/A pointing to the SMF manifests in the proto area that would satisfy each
1933N/A dependency, a list of errors, and a dictionary containing the SMF FMRIs
1933N/A that were contained in the SMF manifest that this action delivers.
1933N/A
1933N/A Note that while we resolve SMF dependencies from SMF FMRIs to the files
1933N/A that deliver them, we don't attempt to further resolve those files to
3356N/A pkg(7) packages at this point.
1933N/A That stage is done using the normal "pkgdepend resolve" mechanism."""
1933N/A
1933N/A if action.name != "file":
1933N/A return [], \
3158N/A [ _("{0} actions cannot deliver SMF manifests").format(
3158N/A action.name)], {}
1933N/A
1933N/A # we don't report an error here, as SMF manifest files may be delivered
1933N/A # to a location specifically not intended to be imported to the SMF
1933N/A # repository.
1933N/A if not has_smf_manifest_dir(action.attrs["path"]):
1933N/A return [], [], {}
1933N/A
1933N/A proto_file = action.attrs[PD_LOCAL_PATH]
1933N/A SMFManifestDependency.populate_cache(action.attrs[PD_PROTO_DIR_LIST])
1933N/A
1933N/A deps = []
1933N/A elist = []
1933N/A dep_manifests = set()
1933N/A
1933N/A instance_mf, instance_deps = parse_smf_manifest(proto_file)
1933N/A if instance_mf is None:
3158N/A return [], [ _("Unable to parse SMF manifest {0}").format(
3158N/A proto_file)], {}
1933N/A
1933N/A for fmri in instance_mf:
1933N/A try:
1933N/A protocol, service, instance = split_smf_fmri(fmri)
1933N/A # we're only interested in trying to resolve
1933N/A # dependencies that instances have declared
1933N/A if instance is None:
1933N/A continue
1933N/A
3171N/A except ValueError as err:
3158N/A elist.append(_("Problem resolving {fmri}: {err}").format(
3158N/A **locals()))
1933N/A continue
1933N/A
1933N/A # determine the set of SMF FMRIs we depend on
1933N/A dep_fmris = set()
1933N/A try:
1933N/A dep_fmris = set(
1933N/A get_smf_dependencies(fmri, instance_deps))
3171N/A except ValueError as err:
1933N/A elist.append(
3158N/A _("Problem determining dependencies for {fmri}:"
3158N/A "{err}").format(**locals()))
1933N/A
1933N/A # determine the file paths that deliver those dependencies
1933N/A for dep_fmri in dep_fmris:
1933N/A try:
2682N/A manifests = resolve_smf_dependency(dep_fmri,
1933N/A instance_mf)
3171N/A except ValueError as err:
1933N/A # we've declared an SMF dependency, but can't
1933N/A # determine what file delivers it from the known
1933N/A # SMF manifests in either the proto area or the
1933N/A # local machine.
2682N/A manifests = []
1933N/A elist.append(
1933N/A _("Unable to generate SMF dependency on "
3158N/A "{dep_fmri} declared in {proto_file} by "
3158N/A "{fmri}: {err}").format(**locals()))
1933N/A
2682N/A if len(manifests) == 1:
2682N/A dep_manifests.add(manifests[0])
2682N/A
2682N/A elif len(manifests) > 1:
2682N/A protocol, service, instance = \
2682N/A split_smf_fmri(dep_fmri)
2682N/A
2682N/A # This should never happen, as it implies a
2682N/A # service FMRI, not an instance FMRI has been
2682N/A # returned from search_smf_dic via
2682N/A # resolve_smf_dependency.
2682N/A if instance is not None:
2682N/A elist.append(
2682N/A _("Unable to generate SMF "
2682N/A "dependency on the service FMRI "
3158N/A "{dep_fmri} declared in "
3158N/A "{proto_file} by {fmri}. "
2682N/A "SMF dependencies should always "
2682N/A "resolve to SMF instances rather "
2682N/A "than SMF services and multiple "
2682N/A "files deliver instances of this "
3158N/A "service: {manifests}").format(
3158N/A dep_fmri=dep_fmri,
3158N/A proto_file=proto_file,
3158N/A fmri=fmri,
3158N/A manifests=", ".join(manifests)))
2682N/A
2682N/A dep_manifests.add(tuple(manifests))
1933N/A
1933N/A for manifest in dep_manifests:
1933N/A deps.append(SMFManifestDependency(action, manifest, pkg_vars,
1933N/A action.attrs[PD_PROTO_DIR]))
1933N/A pkg_attrs = {
3234N/A "org.opensolaris.smf.fmri": list(instance_mf.keys())
1933N/A }
1933N/A return deps, elist, pkg_attrs
1933N/A
1933N/Adef __get_smf_dependencies(deps):
1933N/A """Given a minidom Element deps, search for the <service_fmri> elements
1933N/A inside it, and return the values as a list of strings."""
1933N/A
1933N/A dependencies = []
1933N/A for dependency in deps:
1933N/A fmris = dependency.getElementsByTagName("service_fmri")
1933N/A dep_type = dependency.getAttribute("type")
1933N/A grouping = dependency.getAttribute("grouping")
2291N/A delete = dependency.getAttribute("delete")
1933N/A
1933N/A # we don't include SMF path dependencies as these are often
1933N/A # not packaged files.
1933N/A if fmris and dep_type == "service" and \
2291N/A grouping == "require_all" and \
2291N/A delete != "true":
1933N/A for service_fmri in fmris:
1933N/A dependency = service_fmri.getAttribute("value")
1933N/A if dependency:
1933N/A dependencies.append(dependency)
1933N/A return dependencies
1933N/A
1933N/Adef is_smf_manifest(smf_file):
1933N/A """Quickly determine if smf_file is a valid SMF manifest."""
1933N/A try:
1933N/A smf_doc = minidom.parse(smf_file)
1933N/A # catching ValueError, as minidom has been seen to raise this on some
1933N/A # invalid XML files.
1933N/A except (xml.parsers.expat.ExpatError, ValueError):
1933N/A return False
1933N/A
1933N/A if not smf_doc.doctype:
1933N/A return False
1933N/A
1933N/A if smf_doc.doctype.systemId != \
1933N/A "/usr/share/lib/xml/dtd/service_bundle.dtd.1":
1933N/A return False
1933N/A return True
1933N/A
1933N/Adef parse_smf_manifest(smf_file):
1933N/A """Returns a tuple of two dictionaries. The first maps the SMF FMRIs
1933N/A found in that manifest to the path of the manifest file. The second maps
1933N/A each SMF FMRI found in the file to the list of FMRIs that are declared
1933N/A as dependencies.
1933N/A
1933N/A Note this method makes no distinction between service FMRIs and instance
1933N/A FMRIs; both get added to the dictionaries, but only the instance FMRIs
1933N/A should be used to determine dependencies.
1933N/A
1933N/A Calling this with a path to the file, we include manifest_paths in the
1933N/A first dictionary, otherwise with raw file data, we don't.
1933N/A
1933N/A If we weren't handed an SMF XML file, or have trouble parsing it, we
1933N/A return a tuple of None, None.
1933N/A """
1933N/A
1933N/A instance_mf = {}
1933N/A instance_deps = {}
1933N/A
1933N/A try:
1933N/A smf_doc = minidom.parse(smf_file)
1933N/A # catching ValueError, as minidom has been seen to raise this on some
1933N/A # invalid XML files.
1933N/A except (xml.parsers.expat.ExpatError, ValueError):
1933N/A return None, None
1933N/A
1933N/A if not smf_doc.doctype:
1933N/A return None, None
1933N/A
1933N/A if smf_doc.doctype.systemId != \
1933N/A "/usr/share/lib/xml/dtd/service_bundle.dtd.1":
1933N/A return None, None
1933N/A
1933N/A manifest_path = None
1933N/A
1933N/A if isinstance(smf_file, str):
2501N/A manifest_path = smf_file
1933N/A
1933N/A svcs = smf_doc.getElementsByTagName("service")
1933N/A for service in svcs:
1933N/A
1933N/A fmris = []
1933N/A svc_dependencies = []
1933N/A create_default = False
1933N/A duplicate_default = False
1933N/A
1933N/A # get the service name
1933N/A svc_name = service.getAttribute("name")
1933N/A if svc_name and not svc_name.startswith("/"):
1933N/A svc_name = "/" + svc_name
3158N/A fmris.append("svc:{0}".format(svc_name))
1933N/A else:
1933N/A # no defined service name, so no dependencies here
1933N/A continue
1933N/A
1933N/A # Get the FMRIs we declare dependencies on. When splitting SMF
1933N/A # services across multiple manifests is supported, more work
1933N/A # will be needed here.
1933N/A svc_deps = []
1933N/A for child in service.childNodes:
1933N/A if isinstance(child, minidom.Element) and \
1933N/A child.tagName == "dependency":
1933N/A svc_deps.append(child)
1933N/A
1933N/A svc_dependencies.extend(__get_smf_dependencies(svc_deps))
1933N/A
1933N/A # determine our instances
1933N/A if service.getElementsByTagName("create_default_instance"):
1933N/A create_default = True
1933N/A
1933N/A insts = service.getElementsByTagName("instance")
1933N/A for instance in insts:
1933N/A inst_dependencies = []
1933N/A inst_name = instance.getAttribute("name")
1933N/A fmri = None
1933N/A if inst_name:
1933N/A if inst_name == "default" and create_default:
1933N/A # we've declared a
1933N/A # create_default_instance tag but we've
1933N/A # also explicitly created an instance
1933N/A # called "default"
1933N/A duplicate_default = True
1933N/A
3158N/A fmri = "svc:{0}:{1}".format(svc_name, inst_name)
1933N/A
1933N/A # we can use getElementsByTagName here, since
1933N/A # there are no nested <dependency> tags that
1933N/A # won't apply, unlike for <service> above, when
1933N/A # we needed to look strictly at immediate
1933N/A # children.
1933N/A inst_deps = instance.getElementsByTagName(
1933N/A "dependency")
1933N/A inst_dependencies.extend(
1933N/A __get_smf_dependencies(inst_deps))
1933N/A
1933N/A if fmri is not None:
1933N/A instance_deps[fmri] = svc_dependencies + \
1933N/A inst_dependencies
1933N/A fmris.append(fmri)
1933N/A
1933N/A if create_default and not duplicate_default:
3158N/A fmri = "svc:{0}:default".format(svc_name)
1933N/A fmris.append(fmri)
1933N/A instance_deps[fmri] = svc_dependencies
1933N/A
1933N/A # add the service FMRI
3158N/A instance_deps["svc:{0}".format(svc_name)] = svc_dependencies
1933N/A for fmri in fmris:
1933N/A instance_mf[fmri] = manifest_path
1933N/A
1933N/A return instance_mf, instance_deps
1933N/A
1933N/Adef has_smf_manifest_dir(path, prefix=None):
1933N/A """Determine if the given path string contains any of the directories
1933N/A where SMF manifests are usually delivered. An optional named parameter
1933N/A prefix gets stripped from the path before checking.
1933N/A """
1933N/A global manifest_locations
1933N/A check_path = path
1933N/A if prefix:
1933N/A check_path = path.replace(prefix, "", 1)
1933N/A for location in manifest_locations:
1933N/A if check_path and check_path.startswith(location):
1933N/A return True
1933N/A return False