Easier association providers with libcmpiutil

In CIM, associations are the powerful links between instances that model relationships.  A particular association may relate any number of objects to any number of other objects.  This can make implementation of the association confusing, difficult, and error-prone.

Take, for example, the CIM_ElementCapabilities association.  For an SVPC (Virtualization) provider, you could resolve the association against either half of any of the following pairs of related classes:

  • CIM_ComputerSystem, CIM_EnabledLogicalElementCapabilities
  • CIM_System, CIM_VirtualSystemManagementCapabilities
  • CIM_ResourcePool, CIM_AllocationCapabilities

That can become a lot of work because the provider only gets the association class name (CIM_ElementCapabilities) and the reference passed from the client to do its work.  It seems to be standard practice to solve this problem by subclassing the association superclass for each possible combination of related classes which helps to split up the implementation.  However, this (in my limited experience) tends to result in a lot of duplicated effort, more actual provider modules, etc.  Further, an association provider must implement all four intrinsic methods: Associators(), AssociatorNames(), References(), and ReferenceNames().  Finally, the provider must check the Role, ResultRole, ResultClass, and AssociationClass parameters specified by the client and filter the results of the association appropriately for each of these operations.  If you subclass the association for each pair, even reusing as much code as possible in each provider, you still end up with what I would call a mess.

While developing libvirt-cim, I decided to try to come up with an abstract representation of a single association that relates two specific types.  What I ended up with was the std_association module in libcmpiutil.  This allows you to specify the characteristic details of a particular association (the two class types being associated) and a handler function to do the matching.  The infrastructure does the argument checking, handler dispatch, and result filtering automatically.

Consider the following example association fragment “Foo_SystemDevice” that relates a CIM_System to its devices:

CMPIStatus assoc_sys_to_dev(const char *id,
                            const char *resultClass,
                            const CMPIResult *results)
{
        CMPIStatus s = {CMPI_RC_OK, NULL};
        CMPIInstance *inst;

        if (resultClass == NULL) {
                /* None specified, get all */
                inst = get_disk(id);
                CMReturnInstance(results, inst);

                inst = get_proc(id);
                CMReturnInstance(results, inst);

                inst = get_mem(id);
                CMReturnInstance(results, inst);
        } else if (strcmp(resultClass, “Foo_Disk”Smilie: ;) == 0) {
                inst = get_disk(id);
                CMReturnInstance(results, inst);
        } else if (strcmp(resultClass, “Foo_Processor”Smilie: ;) == 0) {
                inst = get_proc(id);
                CMReturnInstance(results, inst);
        } else if (strcmp(resultClass, “Foo_Memory”Smilie: ;) == 0) {
                inst = get_mem(id);
                CMReturnInstance(results, inst);
        } else {
                /* Unknown resultClass, ignore */
        }

        return s;
}

CMPIStatus assoc_dev_to_sys(const char *id,
                            const char *resultClass,
                            const CMPIResult *results)
{
        CMPIStatus s = {CMPI_RC_OK, NULL};

        if ((resultClass == NULL) ||
            (strcmp(resultClass, “Foo_System”Smilie: ;))) {
                inst = get_system(id);
                CMReturnInstance(results, inst);
        }

        return s;
}

CMPIStatus Associators(CMPIAssociationMI *self,
                       const CMPIContext *context,
                       const CMPIResult *results,
                       const CMPIObjectPath *ref,
                       const char *assocClass,
                       const char *resultClass,
                       const char *role,
                       const char *resultRole,
                       const char **properties)

{
        CMPIData data;
        char *id;
        char *refclass;

        if ((assocClass != NULL) &&
            (strcmp(assocClass, “Foo_SystemDevice”Smilie: ;) != 0))
                return s;

        data = CMGetKey(ref, “InstanceID”, &s);
        id = CMGetCharPtr(data.value);
        refclass = CMGetCharPtr(CMGetClassName(ref, &s));

        /* Implied check for errors here */

        if (strcmp(refclass, “Foo_System”Smilie: ;) == 0)
                return assoc_sys_to_dev(id, results);
        else if (CMClassPathIsA(_BROKER, ref, “CIM_LogicalDevice”, &s))
                return assoc_dev_to_sys(id, results);
}

In addition to the above, the role and resultRole must be checked.  Then, this must be implemented for AssociatorNames(), References() and ReferenceNames().  Further, this whole set would be duplicated between the case of a host system with physical devices, and a virtual system with virtual devices.  When properly done, much of this should be shared between all four functions.  However, all of them are effectively doing the same exact thing: matching a system with its devices (and then presenting it in four different ways), so why have four entry points to this routine that must be implemented?

Going even further, some people would break this down even further such that the mapping between Foo_System and Foo_Disk would be one association, and Foo_System and Foo_Processor would be disctinct.  While breaking things into very small pieces is a very good thing, you’re starting to scatter your implementation all over the place at this point.

With the std_association mechanism in libcmpiutil, the above code and all of the other cases described above can be implemented as such:

static CMPIStatus sys_to_dev(const CMPIObjectPath *ref,
                             struct std_assoc_info *info,
                             struct inst_list *list)
{
        const char *id;

        if (cu_get_str_path(ref, “InstanceID”, &id) != CMPI_RC_OK) {
                /* Error */
        }

        inst_list_add(list, get_disk(id));
        inst_list_add(list, get_proc(id));
        inst_list_add(list, get_mem(id));

        return (CMPIStatus){CMPI_RC_OK, NULL};
}

static CMPIStatus dev_to_sys(const CMPIObjectPath *ref,
                             struct std_assoc_info *info,
                             struct inst_list *list)
{
        const char *id;

        if (cu_get_str_path(ref, “InstanceID”, &id) != CMPI_RC_OK) {
                /* Error */
        }

        inst_list_add(list, get_system(id));

        return (CMPIStatus){CMPI_RC_OK, NULL};
}

static CMPIInstance *make_ref(const CMPIObjectPath *ref,
                              const CMPIInstance *inst,
                              struct std_assoc_info *info,
                              struct std_assoc *assoc)
{
        CMPIStatus s;
        CMPIObjectPath *instop;
        CMPIObjectPath *refop;
        CMPIInstance *refinst;

        instop = CMGetObjectPath(inst, NULL);
        refop = CMNewObjectPath(_BROKER, NS, info->assoc_class, &s);
        refinst = CMNewInstance(_BROKER, op, &s);

        set_reference(assoc, refinst, ref, instop);

        return refinst;
}

static struct std_assoc _sys_to_dev = {
        .source_class = “Foo_System”,
        .source_prop = “GroupComponent”,
        .target_class = “CIM_LogicalDevice”,
        .target_prop = “PartComponent”,
        .handler = sys_to_dev,
        .make_ref = make_ref
};

static struct std_assoc _dev_to_sys = {
        .source_class = “CIM_LogicalDevice”,
        .source_prop = “PartComponent”,
        .target_class = “Foo_System”,
        .target_prop = “GroupComponent”,
        .handler = dev_to_sys,
        .make_ref = make_ref
};

The last several lines are the important bits.  They let you describe the characteristics of the association(s) and the handler function that is responsible for doing the match.  By using a special MIStub macro (instead of something like CMAssociationMIStub), the standard infrastructure is pulled in to implement the intrinsic methods automatically.

Anything returned by the handler functions will be filtered according to resultClass as (and if) specified by the client.  The role and resultRole parameters of the client request will be checked and honored where appropriate.  AssociatorNames(), Associators(), ReferenceNames(), and References() are all supported with no additional work from the provider author.  To extend this to support additional relationships, such as between Bar_System and Bar_Disk, Bar_Processor, and Bar_Memory, only additional handler functions must be written, along with the appropriate descriptor blocks.

We have been using the std_association module of libcmpiutil for quite a while in the development of libvirt-cim and all of our association providers are based on it.  I think the other developers would agree with me that this allows new associations (however complex they may be) to be added very quickly and with few errors.

Category(s): Codemonkeying
Tags:

Comments are closed.