From 3126884ffe36e05fc4785b4cfa980a34fcae1439 Mon Sep 17 00:00:00 2001 From: Michal Minar Date: Fri, 26 Apr 2013 08:34:33 +0200 Subject: added script for online modification of pegasus repo This allows to create and delete instances and classes specified in mof files on-line. Complements cimmof application, which can not do "delete". With this we are able to unregister static filters and classes from pegasus on package removal - if the pegasus is running :-(. --- openlmi-cimmof | 340 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100755 openlmi-cimmof diff --git a/openlmi-cimmof b/openlmi-cimmof new file mode 100755 index 0000000..cbec8d4 --- /dev/null +++ b/openlmi-cimmof @@ -0,0 +1,340 @@ +#!/usr/bin/env python +# Copyright (C) 2012-2013 Red Hat, Inc. All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +""" +Allows to modify Pegasus repository with declarations in mof files. +Pegasus must be running for this script to work. It depends on cimmof +binary, which is online compilator of MOF files for pegasus. + +It works in this way: + 1. cimmof is called on input mof files + 2. its output (xml) is then parsed by pywbem functions producing + CIM objects (instances of ``pywbem.CIMClass`` and + ``pywbem.CIMInstance``) + 3. these objects are then used in calls to + ``{Create,Modify}{Instance,Class}`` + +*Note* that only Class and Instance declarations are supported. + - This is due to limitations in pywbem parser. + - Although this could be avoided by calling wbemexec on generated XML. +""" + +import argparse +import re +import logging +import subprocess +import sys +import pywbem +import xml.dom.minidom as dom + +DEFAULT_NAMESPACE = "root/cimv2" +DEFAULT_CIMMOF = "cimmof" + +RE_COMMENT = re.compile(r'\s*\s*', re.DOTALL) + +logging.basicConfig(level=logging.ERROR, + format="%(levelname)s - %(message)s") +LOG = logging.getLogger(__name__) + +def die(msg, *args, **kwargs): + """ + Exit with error printed to stderr. + """ + LOG.error(msg, *args, **kwargs) + sys.exit(1) + +def xml_cleanup(xml_str): + """ + Return xml string without comments and whitespaces. + """ + # remove comments + without_comments = "".join(RE_COMMENT.split(xml_str)) + # remove whitespaces + return "".join(l.strip() for l in without_comments.split("\n")) + +def get_objects_from_mofs(cimmof, namespace, *mofs): + """ + Call cimmof binary with mofs as input and obtain class/instance + declarations in XML. + + Return list of pywbem CIM abstractions for each declaration. + """ + cmd = [cimmof, '--xml', '-n', namespace] + objects = [] + for mof in mofs: + process = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=sys.stderr) + (out, _) = process.communicate(mof.read()) + parsed_dom = dom.parseString(xml_cleanup(out)) + # we cannot use pywbem.parse_cim because it does not support + # DECLARATION element, but we can parse individual values + for decl in parsed_dom.getElementsByTagName('VALUE.OBJECT'): + # pywbem first makes tupletree from dom: + # (name, attributes, children) + # and from it generates pywbem CIM abstractions + (_name, _attrs, obj) = pywbem.tupleparse.parse_value_object( + pywbem.dom_to_tupletree(decl)) + objects.append(obj) + return objects + +def get_instance_path(conn, namespace, instance, classes): + """ + Obtains a class declaration from cimom for given instance and builds + a path from it. + + :param conn is a wbem connection + :param namespace (``str``) is a target namespace of instance + :param instance (``CIMInstance``) contains class name and is used to get + values of key properties. Is modified by adding path to it. + :param classes (``dict``) is a cache of classes obtained from cimom. + Its items are in form: ``(classname, CIMClass)``. + + Return ``CIMInstanceName``. + """ + if not instance.classname in classes: + classes[instance.classname] = conn.GetClass(instance.classname, + IncludeQualifiers=True, + namespace=namespace) + cls = classes[instance.classname] + keys = [p.name for p in cls.properties.values() if "Key" in p.qualifiers] + path = pywbem.CIMInstanceName(instance.classname, namespace=namespace) + for key in keys: + if not key in instance: + die("instance of %s is missing key property \"%s\"", + instance.classname, key) + path[key] = instance[key] + instance.path = path + return path + +def create_class(conn, cls, namespace, classes, allow_update=False): + """ + Create class or modify it if already present. + + :param classes: (``dict``) a cache of classes obtained from cimom. + :param allow_update: (``bool``) whether to modify existing class. + """ + try: + if not cls.classname in classes: + classes[cls.classname] = conn.GetClass(cls.classname, + IncludeQualifiers=True, + namespace=namespace) + except pywbem.CIMError as err: + if err.args[0] != pywbem.CIM_ERR_NOT_FOUND: + raise + if cls.classname in classes: + if not allow_update: + LOG.error("class %s already exists", cls.classname) + else: + conn.ModifyClass(cls, namespace=namespace) + LOG.info("modified class %s", cls.classname) + else: + conn.CreateClass(cls, namespace=namespace) + LOG.info("created class %s", cls.classname) + +def create_instance(conn, inst, namespace, classes, allow_update=False): + """ + Create instance or modify it if already present. + + :param classes: (``dict``) a cache of classes obtained from cimom. + :param allow_update: (``bool``) whether to modify existing instance. + """ + path = get_instance_path(conn, namespace, inst, classes) + present = None + try: + present = conn.GetInstance(path) + except pywbem.CIMError as err: + if err.args[0] != pywbem.CIM_ERR_NOT_FOUND: + raise + if present is not None: + try: + if allow_update: + conn.ModifyInstance(inst) + LOG.info("modified instance for path %s", path) + else: + LOG.error("instance %s already exists", path) + except pywbem.CIMError as err: + if err.args[0] == pywbem.CIM_ERR_NOT_SUPPORTED: + LOG.error("ModifyInstance() is not supported for class %s," + " please remove the instance first", + inst.classname) + else: + raise + + else: + conn.CreateInstance(inst) + LOG.info("created instance for path %s", path) + +def reorder_objects(cmd, objects): + """ + Reorder classes and instances so that dependent objects are handled + later. + + Classes can depend between each other in two ways: + 1. one inherits from another + 2. one refers to another (associations) + + The first case can be solved by counting number of parents, that are + also to be removed. Class with highest number will be created as last. + + The second one applies only to associations, which can not refer + to each other. Let's just append them after non-associations. + + :param cmd: (``str``) can be "create" or "delete". In latter case the + result is reversed, so the dependent classes/instances are removed + as first. + + *Note* this does not handle dependencies between instances. + """ + cls_list = [] + assoc_list = [] + cls_dict = set( c.classname.lower() + for c in objects if isinstance(c, pywbem.CIMClass)) + # (class name, number of superclasses in cls_dict) + cls_deps = pywbem.NocaseDict() + inst_list = [] + for obj in objects: + if isinstance(obj, pywbem.CIMClass): + if 'association' in obj.qualifiers: + assoc_list.append(obj) + else: + cls_list.append(obj) + cls_deps[obj.classname] = 0 + parent = obj.superclass + while parent in cls_dict: + cls_deps[obj.classname] += 1 + parent = cls_dict[parent].classname + else: # no specific reordering of instances + inst_list.append(obj) + key_func = lambda c: (cls_deps[c.classname], c.classname) + cls_list = sorted(cls_list, key=key_func) + assoc_list = sorted(assoc_list, key=key_func) + + result = cls_list + assoc_list + inst_list + if cmd == "delete": + result.reverse() + return result + +def push_to_repo(namespace, cmd, objects, allow_update=False): + """ + Create or delete desired objects in Pegasus repository. + + :param cmd: (``string``) is one of { 'create' | 'delete' } + :param objects: (``list``) is a list of pywbem CIM abstractions + created from mofs. They will be operated upon. + """ + if not isinstance(namespace, basestring): + raise TypeError("namespace must be string") + if not cmd in ('create', 'delete'): + raise ValueError('cmd must be either "create" or "delete"') + classes = pywbem.NocaseDict() + conn = pywbem.PegasusUDSConnection() + objects = reorder_objects(cmd, objects) + for obj in objects: + try: + if cmd == "create": + if isinstance(obj, pywbem.CIMClass): + create_class(conn, obj, namespace, classes, allow_update) + elif isinstance(obj, pywbem.CIMInstance): + create_instance(conn, obj, namespace, classes, + allow_update) + else: + LOG.error("unsupported object for creation: %s", + obj.__class__.__name__) + + else: + try: + if isinstance(obj, pywbem.CIMClass): + conn.DeleteClass(obj.classname, namespace=namespace) + LOG.info("deleted class %s", obj.classname) + elif isinstance(obj, pywbem.CIMInstance): + path = get_instance_path(conn, namespace, obj, classes) + conn.DeleteInstance(path) + LOG.info("deleted instance %s", path) + else: + LOG.error("unsupported object for deletion: %s", + obj.__class__.__name__) + except pywbem.CIMError as err: + if err.args[0] == pywbem.CIM_ERR_NOT_FOUND: + LOG.warn("%s not present in repository", + obj if isinstance(obj, pywbem.CIMClass) + else path) + else: + raise + + except pywbem.CIMError as err: + if err.args[0] in (pywbem.CIM_ERR_INVALID_PARAMETER, ): + LOG.warn("failed to %s %s: %s", cmd, obj, err) + else: + raise + +def parse_cmd_line(): + """ + Parse command line and return options. + """ + parser = argparse.ArgumentParser( + usage="%(prog)s [options] {create,delete} mof [mof ...]", + description="Allows to create/delete instances and classes" + " declared in MOF files. It operates only on Pegasus broker" + " that needs to be up and running.") + parser.add_argument('--cimmof', default=DEFAULT_CIMMOF, + help="Path to cimmof binary to use.") + #parser.add_argument('--xml', action='store_true', default=False, + #help="Do not execute any action on cimom, just print the" + #" xml to stdout.") + parser.add_argument('-n', '--namespace', default=DEFAULT_NAMESPACE, + help="Target CIM Repository namespace.") + parser.add_argument('-v', '--verbose', action='store_true', + default=False, help="Be more verbosive on output.") + + mof_parser = argparse.ArgumentParser(add_help=False) + mof_parser.add_argument('mof', nargs='+', + type=argparse.FileType('r'), + default=sys.stdin, + help="Mof files containing declarations of classes and instances" + " to be installed or removed from Pegasus broker.") + + command = parser.add_subparsers(title="Operation commands", + dest="command", + help="Operation on declarations.") + create_cmd = command.add_parser('create', parents=[mof_parser], + help='Create instances and classes listed in mof files.') + create_cmd.add_argument('-u', '--allow-update', + action="store_true", default=False, + help="Allow update of class declaration if it already exists.") + command.add_parser('delete', parents=[mof_parser], + help="Delete instances and classes listed in mof files.") + + args = parser.parse_args() + return args + +def main(): + """ + The main functionality of script. + """ + args = parse_cmd_line() + if args.verbose: + LOG.setLevel(logging.INFO) + # parse mofs and build list of pywbem objects + objs = get_objects_from_mofs(args.cimmof, args.namespace, *args.mof) + if not objs: + die("no declarations found!") + push_to_repo(args.namespace, args.command, objs, + getattr(args, 'allow_update', False)) + +if __name__ == '__main__': + main() + -- cgit