Pywbem_mock Demonstration Notebook

The pywbem client includes a package (pywbem_mock) to mock a WBEM server and allow the pywbem client to execute WBEM operations against this fake WBEM server.

This notebook contains examples of creating and using a simple repository and creating and using more complex repositories usingthe DMTF released Schema MOF files.

For more detailed information on the package and its APIs see the pywbem client documentation on read-the-docs which includes both an overview and API descriptions for the pywbem_mock package of pywbem.

Using pywbem_mock to create a simple repository

The pywbem mock package (pywbem_mock) allows a user of the pywbem client to mock a WBEM server so that pywbem WBEM request methods can be executed without having a WBEM server available;

The pywbem mock support consists of the pywbem_mock.FakedWBEMConnection class that establishes a faked connection. That class is a subclass of pywbem.WBEMConnection and replaces its internal methods that use HTTP/HTTPS to communicate with a WBEM server with methods that operate on a local in-memory repository of CIM objects (the mock repository).

As a result, the operation methods of FakedWBEMConnection are those inherited from WBEMConnection, so they have the exact same input parameters, output parameters, return values, and most of the server raised exceptions, as when being invoked on a WBEMConnection object against a WBEM server.

Each FakedWBEMConnection object has its own mock repository. The mock repository contains the same kinds of CIM objects a WBEM server repository contains: CIM classes, CIM instances, and CIM qualifier types (declarations), all contained in CIM namespaces.

Because FakedWBEMConnection operates only on the mock repository, the class does not have any connection-related or security-related constructor parameters.

FakedWBEMConnection has methods that allow the user to add CIM classes, instances and qualifier types to its mock repository and view what is in the mock repository either by defined pywbem CIM objects (CIMClass, CIMInstance, etc. or by compiling MOF files containing the definition of CIM objects. The methods include:

  • compile_mof_file() - Compiles a MOF file into the fake repository
  • compile_mof_string() - Compiles a string containing MOF into the fake repository
  • add_cimobjects() - Inserts CIM bbjects defined with pywbem classes (CIMClass, CIMInstance, etc.) into the defined fake repository.
  • add_namespace() - Defines a CIM namespace in the fake repository
  • display_repository() - Display the namespaces, CIM classes, CIM instances, etc. that are in the Fake repository

CIM instances in the repository can be modified or deleted by using the pywbem WBEMConnection operation methods such as DeleteClass() or ModifyInstance(). Classes and qualifier declarations can be deleted using the corresponding delete methods.

The following cells demonstrate creating a FakedWBEMConnection, populating it repository, accessing the objects created in the repository and deleting the CIMInstances created.

It also demonstrates defining and access a CIM method in the fake repository.

Create a FakedWBEMConnection and repository

The following code demonstrates adding a simple set of qualifier declarations and a CIM class to the Fake repository by compiling their MOF definitions.

In [ ]:
import pywbem
import pywbem_mock

# MOF string defining qualifiers declarations, class, and instance
# This mof will be used throughout this notebook
mof = '''
    Qualifier Key : boolean = false,
        Scope(property, reference),
        Flavor(DisableOverride, ToSubclass);
    Qualifier Description : string = null,
        Scope(any),
        Flavor(EnableOverride, ToSubclass, Translatable);    
    Qualifier In : boolean = true, 
        Scope(parameter), 
        Flavor(DisableOverride, ToSubclass);
    Qualifier OUT : boolean = true, 
        Scope(parameter), 
        Flavor(DisableOverride, ToSubclass);

         [Description ("A dumb test class")]
    class CIM_Foo {
    
            [Key, Description ("The key property")]
        string InstanceID;
        
            [Description ("Some simplistic data")]            
        uint32 SomeData;
        
            [Description ("A method with parameters."   
                          "uint16 as return value")]                          
        uint16 Method1(
          [IN, Description("Optional Input String Param")]
            string InParam,
          // declare IN (false) because default is True  
          [IN(false), OUT, Description("Optional Out Param")]
            uint32 OutParam);
    };

    // Create an instance of CIM_Foo
    instance of CIM_Foo { InstanceID = "I1"; SomeData=3; };
    '''

# Create a faked connection (with a mock repository in full mode)
conn = pywbem_mock.FakedWBEMConnection(default_namespace='root/cimv2')

# Compile the MOF string and add its CIM objects to the default namespace
# of the mock repository
conn.compile_mof_string(mof)

print("Qualifier declarations and classes installed in Fake repository")

Display the created repository

At any time, the data in the repository can be displayed using the method FakedWBEMConnection.display_repository().

This method includes a number of options to display selected namespaces, define the destination for the output, display only a summary of data in the repository, and define the output format for the displayed objects (mof, xml, repr).

In [ ]:
conn.display_repository()

Execute WBEM operations on the mock repository

Once qualifier declarations, classes, and instances have been inserted into the mock repository they can be retrieved using the WBEMConnection methods provided by pywbem.

Thus, WBEMConnection.getQualifier() retrieves a single qualifier declaration. The method tomof() is a method in pywbem cimobject classes CIMQualifierDeclaration, CIMClass, and CIMInstance and is an easy way to display the objects returned from the repository in the standard DMTF language for CIM objects.

In [ ]:
from pywbem import CIMInstanceName, Error
## Perform operations on the faked connection:

# Enumerate top-level classes in the default namespace (without subclasses)
classes = conn.EnumerateClasses();

### Get the 'Description' qualifier type in the default namespace
qd = conn.GetQualifier('Description')
print(qd.tomof())

### Enumerate subclasses of 'CIM_Foo' in the default namespace (without subclasses)
classes = conn.EnumerateClasses(ClassName='CIM_Foo')
for cls in classes:
    print(cls.tomof())

### Get 'CIM_Foo' class in the default namespace
my_class = conn.GetClass('CIM_Foo')

### Get a specific instance of 'CIM_Foo' in the default namespace
keybindings = {'InstanceID': "I1"}
inst = conn.GetInstance(CIMInstanceName('CIM_Foo', keybindings))
print("RESULTS:")
print(inst.tomof())
### print the path of the returned instance
print("path:{0}".format(inst.path))

Create a new instance of the defined class

Creating an instance primarily involves attaching properties with their name, value (and often type) as a dictionary to a CIMInstance with the name of the class for the new instance. The method WBEMConnection.CreateInstance() only requires the new instance object (and the namespace if the default namespace is not being used) to manage the instance that is being created.

The mocker creates the path for this instance and inserts the new instance into the mock repository. Note that a successful CreateInstance() returns a CIMInstanceName for the new instance which can be used to access the new instance on the WBEM server.

In [ ]:
from pywbem import CIMInstance, Uint32

p = {"InstanceID": "I2", "SomeData": Uint32(999)}

newinst = CIMInstance("CIM_Foo", properties=p)
new_path = None
try:
    new_path = conn.CreateInstance(newinst)
    print("Return path: %s" % new_path)
except Error as er:
    print("Exception on CreateInstance. exception=%s" % er)

Retrieve the new instance from the mock server

Here we retrieve the instance we just created from the existing connection and display the instance using the tomof() method.

Note that we are retrieving the instance using the path we previously defined. Since the server returns the path of an instance it creates, that is the path object your should be using to retrieve an instance. Under some circumstances the server could change the path and return a path different that what eyou defined. The mocker does not change the path provided.

In [ ]:
myinst = conn.GetInstance(CIMInstanceName('CIM_Foo', keybindings={'InstanceID': "I2"}))

print("path:%s\n%s" % (myinst.path, myinst.tomof()))

Get the new instance with server defined path

Since WBEMConnection.CreateInstance( returned the path the server created for the instance we inserted into the repository, that path can also be used to retrieve the instance from the repository

In [ ]:
print('Path to new instance %s' % new_path)
myinst2 = conn.GetInstance(new_path)
print(myinst2.tomof())

Retrieve all instances of the class

In this case we display the returned instances using the string representation

In [ ]:
insts = conn.EnumerateInstances("CIM_Foo")
for inst in insts:
    print("path=%s" % inst.path)
    print("%s" % inst)
 

Delete the new instance

The WBEMConnection::DeleteInstance method deletes a single instance of an instanced given the instance path.

In [ ]:
try:
    conn.DeleteInstance(new_path)
    for inst in insts:
        print("path=%s" % inst.path)
except Error as er:
    print("Error with delete")
    

Defining, registering, and executing a mock CIM method

The mocker can execute CIM methods as if they were on a server. However, since there are no real providers in the mocker, the user must define what a method would do in the server. The user-defined provider is defined as a subclass of the MethodProvider class defined in pywbem_mock. This subclass defines"

  • The class(es) for which this provider will be called:
  • a init method to make the CIM repository avaiable.
  • The InvokeMethod method that will be executed.

    This sublcas is provided to the mocker by creating a user-defined provider and registering that provider using the register_provider method.

    The following example defines the user-defined provider (CIM_Foo

In [ ]:
from pywbem import CIMParameter
from pywbem_mock import MethodProvider
import six

# Definition of the user-defined method provider.  This capability is
# new in pywbem 1.0.0. Earlier versions used a callback functionalirty
# that was ver limited.
class TST_ClassProvider(MethodProvider):
    """
    User test provider for InvokeMethod using TST_Class and method1.
    """
    # Names of the class(es) for which this provider is to be registered
    provider_classnames = 'TST_Class'

    def __init__(self, cimrepository):
        super(TST_ClassProvider, self).__init__(cimrepository)

    def InvokeMethod(self, namespace, MethodName, ObjectName, Params):
        """
        Simplistic test method withn TST_Class.
        """
        # Basic validity tests including 
        # * valid namespace,
        # * validity of the ObjectName type, 
        # * existence of the ObjectName in the repository
        # * existence of the MethodName in the class defined by the ObjectName 
        # have already be completed before this method is called.  
        # The user method need only:
        # * process input parameters
        # * execute any provider specific code (what ever the method was to do)
        # * create any output parameters
        # * create a return value
        # * return the return_value and return parameters
        
        print('Input Parameters {}'.format(Params))

        # Execute provider specific code and define any return parameters and
        # a return value
        out_params = [CIMParameter('OutputParam1', 'string', value=3)]
        return_params = out_params
        return_value = 0

        return (return_value, return_params)

class_with_method_mof = '''
    Qualifier Out : boolean = false,
        Scope(parameter),
        Flavor(DisableOverride, ToSubclass);

    Qualifier Static : boolean = false, 
        Scope(property, method), 
        Flavor(DisableOverride, ToSubclass);

    class TST_Class {

        string InstanceID;

          [Static,
           Description("Static method with input and output parameters")]
        uint32 Method1(
            [IN, Description("Input param 1")]
          string InputParam1,
            [IN (False), OUT, Description("Output param 1")]
          string OutputParam1);
    };
'''

# Compile the MOF string and add its CIM objects to the default namespace
# of the mock repository
conn.compile_mof_string(class_with_method_mof)

# Register the method provider defined above, for the
# default namespace of the connection

# Try block allows excuting cell multiple times without exception
# because the provider is already registered on subsequent calls.
try:
    conn.register_provider(TST_ClassProvider(conn.cimrepository),
                           namespaces=None,
                           schema_pragma_files=None, verbose=None)
except Exception as ve:
    print(ve)
    
# display the user providers registered
print("Display of registered providers")
conn.display_registered_providers()
print("===============")

# Define a value for the Method Parameter InpurParam1
print("Execute the user provider method:\n")
params = [('InputParam1', 'someData')]
# Invoke static method Method1
result = conn.InvokeMethod('Method1', 'TST_Class', Params=params)

print('\nReturn value: %r' % result[0])
print('Output parameters: %r' % (result[1],))

Creating a repository from DMTF Schema

In addition to creating a repository with specifically defined classes, qualifier declarations and instances the repository can be created using the published DMTF schema as the source for qualifier declarations and classes. The DMTF schema is released on a regular basis with new functionality, fixes, etc. And is published on the DMTF web site. It includes the set of CIM qualifier declarations defined by the DMTF and the CIM classes that make up the CIM model. It is available in both MOF and XML formats. The method documented here uses the MOF files as the basis for creating CIM qualifier declarations and CIM classes in the mock repository.

Using this method requires that you make the following decisions:

  • Which version of the published DMTF released schema you intend to use? The available Schema versions are listed on the DMTF web site

  • Where in your local environment do you want to save and expand the schema. This should be a directory and serves both as the storage space for download and expansion and the location checked before download so that the schema is not downloaded each time it is used?

  • Use of the experimental schema for the defined DMTF schema release as well as the released schema.

  • The leaf classes in the class hiearchy you want to mock. The method takes responsibility for determining superclasses and other required classes ( ex. associations, embedded instances) defined in the MOF for any of the leaf classes you specify.

The method FakedWBEMConnection.compile_dmtf_schema() performs a number of functions including:

  • Downloading the DMTF Schema defined by the version if it is not already downloaded.
  • Creating a list of all classes that must be compiled based on the list of classes provided (the leaf classes required for the mock)
  • Compiling all of the qualifier declarations defined in the CIM Schema.
  • Compiling the defined classes and classes on which they depend into the repository.

The following example defines a DMTF schema version, the destination directory, and the leaf classes of interest and then calls the compile_dmtf_schema method to create a mock repository. At the end of the operation, the mock repository is includes the complete DMTF set of qualifier declarations (there are only about 100 of them) and the classes you specified and their dependencies. The destination directory should also include the complete download from the DMTF and the expanded mof files for the qualifier declarations and classes.

Note: that each time you call compile_dmtf_schema() it installs the complete set of qualifier declarations in the mock repository so that it is best to create a complete list of leaf classes required an call this method only once.

Warning: Some of these cells create new objects on the server. Once those objects are created, they cannot simply be recreated on the server so the repeated execution of some of the cells without restarting (see Kernel in the menu) will cause exception returns from the server.

In [ ]:
import pywbem
import pywbem_mock

import os
print(os.getcwd())

# Defines the version of the DMTF schema to be installed.
# This demo is done with the schema already loaded into the pywbem development environment assuming that
# the user is using the docs/notebooks directory. This may change as pywbem updates the schema use
# in out tests
DMTF_TEST_SCHEMA_VER = (2, 49, 0)
# location of schema 2.49.0 in pywbem 0.13.0 development
# code cloed from github
SCHEMA_DIR = os.path.join('..', '..', 'tests', 'schema')
print(SCHEMA_DIR)

# An alternative would be to define your own schema and schema directory
# The schema will be downloaded and installed on the first use.
#DMTF_TEST_SCHEMA_VER = "2.51.0"
#SCHEMA_DIR = "."

classnames = ['CIM_RegisteredProfile', 'CIM_Namespace', 'CIM_ObjectManager',
              'CIM_ElementConformsToProfile', 'CIM_ReferencedProfile']

conn = pywbem_mock.FakedWBEMConnection(default_namespace='root/cimv2')
print(conn)

# The following methods were added in pywbem 1.0.0. To execute this code
# with earlier versions of pywbem replace the two methods with the following
# which was removed in pywbem 1.0/0
#
#    conn.compile_dmtf_schema(DMTF_TEST_SCHEMA_VER, SCHEMA_DIR,
#                                 class_names=classnames, verbose=False)

schema = pywbem_mock.DMTFCIMSchema(DMTF_TEST_SCHEMA_VER, SCHEMA_DIR,
                                   verbose=True)

conn.compile_schema_classes(classnames,
                            schema.schema_pragma_file,
                            verbose=True)

conn.display_repository()

Add more classes to this repository

Subsequent to the installation of the DMTF schema we can add classes of our own to the repository. This adds two dummy classes that really do not depend on the schema and simply demonstrates installing classes.

In [ ]:
mof= '''
    class TST_Class1 {
          [Key]
        string InstanceID;
        string Prop1;
    };

    class TST_Class2 {
          [Key]
        string InstanceID;
        string Prop2;
    };

      [Association]
    class TST_Association12 {
          [Key]
        TST_Class1 REF Ref1;
          [Key]
        TST_Class2 REF Ref2;
    };
'''

conn.compile_mof_string(mof)
# print the class returned from the server using the __str__ magic method and __repr__ magic method
print(conn.GetClass("TST_Class1"))
print(repr(conn.GetClass("TST_Class2")))

Add more CIM objects

Now that we have classes in the repository we can add some CIM Instances. The following cell defines an instance of each of the new classes and also an assoiation instance that relates the instances of the classes.

In this case, they are defined as instances of pywbem objects to demonstrate adding pywbem CIM objects to the repository.

In [ ]:
ns = conn.default_namespace

# Define a key for this instance
c1_key = pywbem.CIMProperty('InstanceID', type='string', value='111')

# Create the path for the instance (its CIMInstanceName)
c1_path = pywbem.CIMInstanceName(
    'TST_Class1',
    keybindings={c1_key.name: c1_key.value},
    namespace=ns
)

# Create a CIMInstance of class TST_Class1
c1 = pywbem.CIMInstance(
    'TST_Class1',
    properties=[
        c1_key,
        pywbem.CIMProperty('Prop1', type='string', value='1'),
    ],
    path=c1_path,
)

# Create a second instance
c2_key = pywbem.CIMProperty('InstanceID', type='string', value='222')
c2_path = pywbem.CIMInstanceName(
    'TST_Class2',
    keybindings={c2_key.name: c2_key.value},
    namespace=ns
)
c2 = pywbem.CIMInstance(
    'TST_Class2',
    properties=[
        c2_key,
        pywbem.CIMProperty('Prop2', type='string', value='2'),
    ],
    path=c2_path,
)

# Create keys and paths for CIMInstance 1 and 2
a12_key1 = pywbem.CIMProperty('Ref1', type='reference', value=c1_path)
a12_key2 = pywbem.CIMProperty('Ref2', type='reference', value=c2_path)
a12_path = pywbem.CIMInstanceName(
    'TST_Association12',
    keybindings={
        a12_key1.name: a12_key1.value,
        a12_key2.name: a12_key2.value,
    },
)

# Define the association instance
a12 = pywbem.CIMInstance(
    'TST_Association12',
    properties=[
        a12_key1,
        a12_key2,
    ],
    path=a12_path,
)

# add all of the created CIM instances to the repository
conn.add_cimobjects([c1, c2, a12])

# Get the instances from the repository and display them
returned_instances = conn.EnumerateInstances('TST_Class2')
for inst in returned_instances:
    print(inst.tomof())

The association operations

The relation between a source instance and it associated instances is defined with instances of an association class (CIMClass with Association qualifier) and can be accessed through the References() and Associators() WBEMConnection methods. These are powerful methods that provide the basis for navigating through the CIM model on a server.

Note that in most cases an association can also be accessed directly by executing GetInstance or EnumerateInstances on the Association class. However, the power of the References() and Associators() methods is that the return instances within the class hiearchy defined by association instances including optionally accounting for subclasses, filtering of the roles (Reference property names) and the Association class name.

Pywbem_mock accesses associations defined in the mock repository with the WBEMConnection.Associators() and WBEMConnection.References() methods and implements the same retrieval algorithms as a WBEM server to get the correct instances based on the request input parameters.

In [ ]:
# Display the instances of the test association.
returned_assoc_insts = conn.EnumerateInstances("TST_Association12")
for inst in returned_assoc_insts:
    print(inst.tomof())

# Get the References for each of the target instances defined for
# the association.
cls_111_paths = conn.EnumerateInstanceNames('TST_Class1')
for path in cls_111_paths:
    returned_refs = conn.References(path)
    print('Refs for %s' % path)
    for inst in returned_refs:
        print(inst.tomof())
        
# Get the associated instances for the target instances using the
# Associators operation
for path in cls_111_paths:
    returned_assocs = conn.Associators(path)
    print('Associations for %s' % path)
    for inst in returned_assocs:
        print(inst.tomof())
    for inst in returned_assocs:
        print('Returned association path: %s:' % inst.path)
In [ ]: