#!/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()