summaryrefslogtreecommitdiffstats
path: root/openlmi-cimmof
blob: cbec8d43cff339801a3f975e8c83863028d46a57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
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()