Novell is now a part of Micro Focus

How to Extend the NetWare Scripting Environment by Creating UCX Components

Articles and Tips: article

Srivathsa M
Manager Software Engineering
Novell Bangalore
msrivathsa@novell.com

01 Mar 2002


This AppNote showcases the creation of a Universal Component Extensions (UCX) component to access Lightweight Directory Access Protocol (LDAP) services, and demonstrates the usage of this component from the scripting languages.


Topics

NetWare 6, scripting, Novell Script for NetWare (NSN), Perl, Universal Component eXtensions (UCX), Universal Component System (UCS)

Products

NetWare 6

Audience

developers

Level

intermediate

Prerequisite Skills

familiarity with scripting, and NLM programming

Operating System

NetWare 6

Tools

Novell Script for NetWare, NLM development tool such as CodeWarrior or Watcom, LDAP SDK for C

Sample Code

Yes

Introduction

Scripting products such as Novell Script for NetWare (NSN), Perl, Universal Component eXtensions (UCX), and Universal Component System (UCS) are part of NetWare 6. These products help in easy and quick application development on NetWare. The UCX components shipped with NSN which exposes some of the common NetWare services and these can be used from scripting languages. The capabilities of the scripting infrastructure are not limited. It can be extended to provide access to more services. You don't have to wait for Novell to provide new components to satisfy your requirements. Instead, you can create your own components by making use of the UCX SDK. You can also distribute your component. This article showcases the creation of an UCX component to access LDAP services and the usage of this component from the scripting languages.

Software Requirements

The following software is required for creation of the UCX component demonstrated in this article. The documentation for these products is available at the respective URL.

Component requirements

The first step in component creation is the proper identification of the problem to be solved or the service to be exposed. Then identify the specific features, which are necessary in the component.

For our example we want to create an UCX component for accessing LDAP services that can be used for searching the LDAP directories.

Component Development

Component development starts with the proper design. Design of the component should be such that the component is easily usable and follows the standard design conventions used for the creation of components such as Java Beans, ActiveX controls, other UCX components. External interface of the component should be easy to use and the complexities of programming to the underlying service should be hidden inside the component. This can be achieved by identification of properties, methods, events, and relationship between the objects if there are sub objects in the component.

Now let us consider some of the features required in our component for LDAP services. We will name the component as UCX:LDAP. To identify an LDAP server, the component should have a server name and server port properties. Login and Logout methods are required to bind and unbind from the LDAP server. A search method is required to search the directory. The ability to search based on the search filter is an additional feature. To represent the search result, the component should support a collection of entry objects where each entry represents one object in the LDAP directory.

Based on this we can arrive at the simple object diagram below.

Object Diagram of LDAP Component.

The design of the component can be explained as shown below.

LDAP component design.
   
Component Name: UCX:LDAP
   
This component exposes the following constants:
Search Scope constants:
Constant Name                  Constant Type               Constant Value
   
LDAP_SCOPE_BASE                  Integer               0
LDAP_SCOPE_ONELEVEL                  Integer               1
LDAP_SCOPE_SUBTREE                  Integer               2
   
Properties:
   String Server                  Read/Write:
   Represents the name of the LDAP 
                                 server. Default Value: ""
   Long Port                  Read/Write:             
   Represents the LDAP server port. 
                                 Default value 389.
   String LoginName                  Read Only:             
   The name of the user who is logged
                                 in. Default Value: ""
   
Methods:
   Bool Login(String username, String password)
         This method logs into the LDAP server and sets the
         LoginName property. In case of error sets the global error
         object and returns false.
   
   Bool Logout()
         Logs out of the LDAP server.
   
   Entries Search (String SearchFilter, String SearchBase, Integer SearchScope)
   
      SearchFilter: Standard LDAP search filter expression.
      SearchBase: The base context for search.
      SearchScope: The scope of search. Can be one of the search
                  scope constants.
   
         Searches the directory and returns a collection of entry
         objects. In case of error, returns NULL and sets the err
         object.
               
LDAPEntries object:
Properties:
   Long count            Read Only: Number of objects in the collection.
   
Methods:
   Void      Reset():Resets the collection.
   Entry       Next(): Fetches the next element from the collection.
   Bool      HasMoreElements(): Returns true if the collection contains
                           some more elements.
   
LDAPEntry Object:
   
Properties:
   String FullName               Read Only/Default Property:
                  Fully distinguished name of the LDAP object.
   
Methods:
      Nil

The methods Reset, Next, and HasMoreElements in the LDAPEntries object ensure the easy navigation through the collection using the For Each <Object> in <Collection> syntax of NSN.

Converting the Design to Code

The coding involves the declaration of constants, properties, methods, and the library procedure for each object.Then the actions specific to the property or method have to be incorporated.

Declare the Methods

For each method declare the method signature consisting of the return value and arguments as explained in the UCX SDK documentation. Then create an UCXMethod list. Ensure that the entries are arranged in the ascending order of their name in the method list.

The following code sample shows the method declarations for the LDAP object.

// Parameter description : UCX:LDAP Methods
UCXParameter LDAPLoginParams[] = {
   {UCX_BOOLEAN_TYPE,            "result"},
   {UCX_STRING_TYPE,            "userName"},
   {UCX_OPTIONAL_TYPE,                "password"},
   {NULL,            NULL}
};
   
UCXParameter LDAPLogoutParams[] = {
   {UCX_BOOLEAN_TYPE,            "result"},
   {NULL,            NULL}
};
   
UCXParameter LDAPSearchParams[] = {
   {UCX_OBJECT_TYPE,         "entries"},
   {UCX_STRING_TYPE,         "searchFilter"},
   {UCX_STRING_TYPE,         "searchBase"},
   {UCX_LONG_TYPE,         "searchScope"},
   {NULL,         NULL}
};
   
enum
{
   LDAP_LOGIN,
   LDAP_LOGOUT,
   LDAP_SEARCH,
};
   
// Methods
UCXMethod LDAPMethods[] = {
   {"LOGIN",         LDAP_LOGIN,            LDAPLoginParams},
   {"LOGOUT",         LDAP_LOGOUT,            LDAPLogoutParams},
   {"SEARCH",         LDAP_SEARCH,            LDAPSearchParams},
   {NULL,         NULL,            NULL}
};

Declare the Properties

Declare the properties in the property list. Each entry in the property list contains the name, data type and the visibility of the property. These lists are always terminated with a null element. The properties declared with visibility UCX_PRIVATE are internal to the component and cannot be accessed from the scripts. These can be used to store the private information.

The following code sample shows the property declaration for LDAP object. To preserve the LDAP handle we can make use of a private property LDAPHANDLE. The handle can be retrieved from this property and can be used in subsequent LDAP calls.

// Properties
UCXProperty LDAPPropertyList[] = {
   {UCX_POINTER_TYPE,               "LDAPHANDLE",               UCX_PRIVATE},
   {UCX_STRING_TYPE,                "SERVER",              UCX_PUBLIC},
   {UCX_STRING_TYPE,                "PORT",              UCX_PUBLIC},
   {UCX_STRING_TYPE,                "LOGINNAME",              UCX_PUBLIC},
   {NULL,                          NULL,                  NULL}
};
Constant Declaration

Components can expose constants. These constants can be directly used from the scripting languages. The following are the search scope constants of our component.

// Constants
UCXConstant LDAPScopeConstants[] = {
   {UCX_INTEGER_TYPE,            "LDAP_SCOPE_BASE",            "0"},
   {UCX_INTEGER_TYPE,            "LDAP_SCOPE_ONELEVEL",            "1"},
   {UCX_INTEGER_TYPE,            "LDAP_SCOPE_SUBTREE",            "2"},
   {NULL,               NULL,            NULL}
};

Complete the above steps for each object. The following list contains all these declarations.

// LDAPEntries
UCXParameter LDAPEntriesResetParams[] = {
   {UCX_BOOLEAN_TYPE,              "void"},
   {NULL,              NULL}
};
UCXParameter LDAPEntriesNextParams[] = {
   {UCX_OBJECT_TYPE,           "entry"},
   {NULL,           NULL}
};
UCXParameter LDAPEntriesHasMoreElementsParams[] = {
   {UCX_BOOLEAN_TYPE,              "void"},
   {NULL,              NULL}
};
enum
{
   ENTRIES_HASMORELEMENTS,
   ENTRIES_NEXT,
   ENTRIES_RESET
};
   
UCXMethod LDAPEntriesMethods[] = {
{"HASMOREELEMENTS", ENTRIES_HASMORELEMENTS, LDAPEntriesHasMoreElementsParams},
   {"NEXT",            ENTRIES_NEXT,               LDAPEntriesNextParams},
   {"RESET",            ENTRIES_RESET,               LDAPEntriesResetParams},
   {NULL,            NULL,               NULL}
};
UCXProperty LDAPEntriesPropertyList[] = {
   {UCX_POINTER_TYPE,             "SEARCHHANDLE",               UCX_PRIVATE},
   {UCX_LONG_TYPE,                "COUNT",                  UCX_PUBLIC},
   {NULL,                         NULL,                  NULL}
};
   
// LDAP Entry
UCXProperty LDAPEntryPropertyList[] = {
   {UCX_STRING_TYPE,         "FULLNAME",            UCX_PUBLIC},
   {NULL,             NULL,            NULL}
};
   
// Entries private structure to store search handle
typedef struct tagUCXENTRIES{
   LDAP        *ld;
   LDAPMessage *searchResult;
   LDAPMessage *lastEntry;
   LDAPMessage *nextEntry;
} UCXENTRIES, *PUCXENTRIES;

The next step is to combine all these elements together to create the definition for all the classes. You can pass NULL for those fields, which are not required for the component.

The following code sample shows the class list structure of the LDAP component.

// LDAP class definition
static UCXClass LDAPClassList[] =                  {
   {"UCX:LDAP", 0, 0, 0, 0, &MyLib, LDAPMethods, NULL, LDAPPropertyList,
      UCXLDAPLibProc,NULL},
   {"UCX:LDAP.LDAPENTRIES", 0, 0, 0, 0, &MyLib, LDAPEntriesMethods, NULL,
      LDAPEntriesPropertyList, UCXLDAPEntriesLibProc,NULL},
   {"UCX:LDAP.LDAPENTRY", 0, 0, 0, 0, &MyLib, NULL, NULL, LDAPEntryPropertyList,
      UCXLDAPEntryLibProc,"FULLNAME"},
   {NULL}
};

Now include standard UCX macros, and a main function which registers the component.

// Standard macros
UCXSTDPROCS();
UCXDEFINELIB();
...
UCXENTRY(NWDIR)
{
   UCXINITLIBRARY2(LDAPClassList, "LDAP",
      "LDAP UCX Component",
      "-------------------",
      "Copyright 2001-2002 Novell Inc., All Rights Reserved");
   UCXREGISTERLIBRARY();
}
   
UCXUNLOADERPROC();

Once you are ready with the property/method declaration, the next step is to include the necessary code, which gets executed whenever the property/method is invoked. This is done through the library procedure.

How Library Procedure Works

The communication between the UCX Library Manager and the component is through predefined UCX events. Whenever a property or method is invoked, UCX library manager sends a relevant event to the library procedure. Handle this event to service a particular call. Some of the common event handling activities, which are applicable to most of the components, are:

  • UCX_EVENT_OBJECT_CREATE for allocation of any private data, initialization of the properties to the default values. UCX_EVENT_OBJECT_DESTROY for performing necessary cleanup.These events can be compared to the constructor and destructor of objected oriented paradigm.

  • UCX_EVENT_PROPERTY_GET and UCX_EVENT_PROPERTY_SET for retrieving and setting the property values.

  • UCX_EVENT_EXEC for servicing a particular method invoke request.

  • Dispatching of any unhandled events to the default event handler, UCXDispatchEvent.

The following code sample shows the library procedure for the LDAP component.

UCX_API UCXLDAPLibProc(void *CP, void *params, CHAR_PTR FN, uint32 Event,
UCXClassLink *ClassLink, void *Object)
{
int index=0;
LDAP *ld=NULL;
   
   switch( Event )
   {
   case UCX_EVENT_OBJECT_CREATE:
         UCXSetIntegerProperty (CP, Object, "PORT", 389);
         UCXSetPointerProperty (CP, Object,"LDAPHANDLE", NULL);
         return UCXCreateBoolean(CP, TRUE,FN);
      break;
   case UCX_EVENT_OBJECT_DESTROY:
         ld = (LDAP *)UCXGetPointerProperty (CP, Object, "LDAPHANDLE");
         if (ld != NULL){
            ldap_unbind_s( ld );  
      }
   break;
   
   case  UCX_EVENT_CONSTANT:
         return LDAPScopeConstants;
   
   case  UCX_EVENT_PROPERTY_GET:
         break;
      
   case  UCX_EVENT_PROPERTY_SET:
      if ((stricmp((char const *)FN,"LOGINNAME") == 0)){
         UCXSetErrorText(CP, -300, FN, "Property is Readonly");
         return NULL;
         }
      break;
   
   case UCX_EVENT_EXEC:
      index = UCXMethodIndex(CP, ClassLink->Class, FN);
      if( index == -1 )      /* didn't find the function */
         break;
      switch(index)
      {
      case LDAP_LOGIN:
            return Login(CP,FN,params,Object);
      case LDAP_LOGOUT:
         ld = (LDAP *)UCXGetPointerProperty (CP, Object,
                                    "LDAPHANDLE");
         if (ld != NULL){
            ldap_unbind_s( ld );  
            UCXSetPointerProperty (CP, Object,"LDAPHANDLE", NULL);
         }
         return UCXCreateBoolean(CP,TRUE,FN);
   
      case LDAP_SEARCH:
         return Search(CP,FN,params,Object);
      }
   }
   
   // Dispatch Events that are not used
   return(UCXDispatchEvent(CP, params, FN, Event, ClassLink, Object));
}

Retrieving Arguments and Creating Return Values

Arguments for the methods or the assignment values to the properties can be extracted using the UCXGet<Type>Param functions. For example UCXGetStringParam can be used to retrieve the string argument. Component return values have to be of UCX data types, and can be created using the UCX create functions.For example, UCXCreateBoolean can be used for creation of a boolean type. The following code sample shows all of these.

UCX_API Login(void *CP, CHAR_PTR FN, void *Params, void * Object)
{
char *pszName=NULL, *pszPassword="", *pszHost=NULL;
int ldapPort=389,version,rc=0;
LDAP *ld=NULL;
   
   pszName = UCXGetStringParam(CP, Params,1);
   pszPassword = UCXGetStringParam(CP, Params,2);
   pszHost = UCXGetStringProperty (CP, Object, "SERVER");
   UCXGetIntegerProperty (CP, Object, "PORT", &ldapPort);
   
   /* Set LDAP version to 3 */
   version = LDAP_VERSION3;
   ldap_set_option( NULL, LDAP_OPT_PROTOCOL_VERSION, &version);
   
   /* Initialize the LDAP session */
   if (( ld = ldap_init( pszHost, ldapPort )) == NULL)
   {
         UCXSetErrorText(CP, -1, FN, "LDAP Initialization Failed");
         return UCXCreateBoolean(CP,FALSE,FN);
      }
   
   /* Bind to the server */
   rc = ldap_simple_bind_s( ld, pszName, pszPassword );
   if (rc != LDAP_SUCCESS ){
         UCXSetErrorText(CP, rc, FN, ldap_err2string(rc));
         ldap_unbind_s ( ld );
         return UCXCreateBoolean(CP,FALSE,FN);
   }
   UCXSetPointerProperty (CP, Object,"LDAPHANDLE", ld);
   UCXSetStringProperty (CP, Object,"LOGINNAME", pszName);
   return UCXCreateBoolean(CP,TRUE,FN);
}

Error Handling

The general convention is to set the run time error in a global error object. It is up to the script to either stop the script execution for the runtime errors or to resume the script execution, ignoring the error. The error information can be set using the UCXSetErrorText function.To complete the component development, include the library procedures for all the classes. The following includes the remaining code required for the completion of the LDAP component.

UCX_API Search(void *CP, CHAR_PTR FN, void *Params, void * Object)
{
char *pszFilter=NULL, *pszBase=NULL;
int  searchScope=0, rc=0;
LDAP *ld=NULL;
LDAPMessage *searchResult=NULL;
void *subObj=NULL;
void *pEntriesParams=NULL;
char *attrs[]={ LDAP_NO_ATTRS, NULL };
PUCXENTRIES pEntries=NULL;
   
      pszFilter = UCXGetStringParam(CP, Params,1);
      pszBase = UCXGetStringParam(CP, Params,2);
      searchScope = UCXGetIntegerParam(CP, Params,3);
      ld = (LDAP *)UCXGetPointerProperty (CP, Object, "LDAPHANDLE");
   
   /* Search the directory */
   rc = ldap_search_ext_s
(                   ld,              /* LDAP session handle */
                  pszBase,         /* container to search */
                  searchScope,                      /* search scope */
                  pszFilter,       /* search filter */
                  attrs,           /* Attributes*/
                  0,               /* return attributes and values */
                  NULL,            /* server controls */
                  NULL,            /* client controls */
                  LDAP_NO_LIMIT,   /* time out */
                  LDAP_NO_LIMIT,   /* no size limit */
                  &searchResult ); /* returned results */
   if ( rc != LDAP_SUCCESS ){
      UCXSetErrorText(CP, -3, FN, ldap_err2string( rc ));
      return UCXCreateBoolean(CP,FALSE,FN);
   }   
   pEntries = (PUCXENTRIES)malloc (sizeof (UCXENTRIES));
   if (pEntries){
      pEntries->ld = ld;
      pEntries->searchResult = searchResult;
   }
   else{
      UCXSetErrorText(CP, ERR_OUT_OF_MEMORY, FN, "Server out of 
                  memory");
      return NULL;
   }
   pEntriesParams = UCXCreateParam (CP, FN, UCX_LONG_TYPE, pEntries, 
                              NULL,NULL);
   if( (subObj = UCXInstantiateObject(CP, "UCX:LDAP.LDAPEntries", FN,
         pEntriesParams )) == NULL )
      UCXSetErrorText(CP, ERR_INSTANTIATION, FN, "Error
                     Instantiating Entries collection");
   
   UCXDestroyParam(CP,pEntriesParams);
   return subObj;
}
static void Reset(void *CP, CHAR_PTR  FN, PUCXENTRIES pEntries);
static UCX_API Next(void *CP, CHAR_PTR  FN, PUCXENTRIES pEntries);
static UCX_API HasMoreElements(void *CP, CHAR_PTR  FN, PUCXENTRIES pEntries);
   
UCX_API UCXLDAPEntriesLibProc(void *CP, void *params, CHAR_PTR FN, uint32
	Event, UCXClassLink *ClassLink, void *Object)
{
PUCXENTRIES pEntries=NULL;
int index=0;
   switch( Event )
   {
      case UCX_EVENT_OBJECT_CREATE:
         {
         unsigned long entryCount=0;
         pEntries=(PUCXENTRIES)UCXGetLongParam (CP,params,1);
         if (pEntries == NULL)
            return UCXCreateBoolean(CP, FALSE, FN);
   
         pEntries->lastEntry = NULL;
         pEntries->nextEntry = NULL;
         UCXSetPointerProperty (CP, Object,"SEARCHHANDLE", pEntries);
         entryCount = ldap_count_entries( pEntries->ld,
                              pEntries->searchResult );
         UCXSetLongProperty(CP,Object, "COUNT", entryCount);
         return UCXCreateBoolean(CP, TRUE, FN);
         }
   
      case UCX_EVENT_OBJECT_DESTROY:
         if( (pEntries = (PUCXENTRIES) UCXGetPointerProperty(CP, Object,
"SEARCHHANDLE")) != NULL ){
            ldap_msgfree( pEntries->searchResult);
            free (pEntries);
         }
         break;
   
   case  UCX_EVENT_PROPERTY_GET:
      break;
      
   case  UCX_EVENT_PROPERTY_SET:
      if (stricmp((char const *)FN,"COUNT") == 0)
         return NULL;
      break;
      
   case UCX_EVENT_EXEC:  // function dispatch from lib
      index = UCXMethodIndex(CP, ClassLink->Class, FN);
      if( index == -1 )      /* didn't find the function */
         break;
      
      if( (pEntries = (PUCXENTRIES) UCXGetPointerProperty(CP, Object,
"SEARCHHANDLE")) == NULL )
         return UCXCreateBoolean(CP, FALSE, FN);
      switch (index)
      {
      case ENTRIES_HASMORELEMENTS:
         return HasMoreElements (CP,FN,pEntries);
      case ENTRIES_NEXT:
         return Next (CP,FN,pEntries);
      case ENTRIES_RESET:
         Reset (CP,FN,pEntries);
         return UCXCreateBoolean(CP, TRUE, FN);
      }
   }
   // Dispatch Events that are not used
   return(UCXDispatchEvent(CP, params, FN, Event, ClassLink, Object));
}
   
void Reset(void *CP, CHAR_PTR  FN, PUCXENTRIES    pEntries)
{
   pEntries->lastEntry = NULL;
   pEntries->nextEntry = NULL;
   return;
}
   
UCX_API Next(void *CP, CHAR_PTR  FN, PUCXENTRIES pEntries)
{
   char  *pszObjectName = NULL;
   void *subObj = NULL;
   
   if ( pEntries->nextEntry == NULL){
      if (pEntries->lastEntry == NULL)
         pEntries->lastEntry = ldap_first_entry( pEntries->ld,
                           pEntries->searchResult );
      else
         pEntries->lastEntry = ldap_next_entry( pEntries->ld,
                           pEntries->lastEntry);
   }
   else{
      pEntries->lastEntry = pEntries->nextEntry;
      pEntries->nextEntry = NULL;
   }
   
   if (pEntries->lastEntry == NULL){
      UCXSetErrorText(CP, -100, FN, "No more entries");
      Return NULL;
}
   else{
      if( (subObj = UCXInstantiateObject(CP, "UCX:LDAP.LDAPENTRY", FN, NULL ))
== NULL )
         UCXSetErrorText(CP, ERR_INSTANTIATION, FN, "Error
                        Instantiating LDAPENTRY");
      else{
         pszObjectName = ldap_get_dn( pEntries->ld,
                   pEntries->lastEntry );
         if (pszObjectName){
            UCXSetStringProperty (CP, subObj,
                  "FULLNAME",pszObjectName);
            ldap_memfree( pszObjectName );
         }
      }
      return subObj;
   }
}
   
UCX_API HasMoreElements(void *CP, CHAR_PTR  FN, PUCXENTRIES
                              pEntries)
{
   if (pEntries->lastEntry == NULL)
      pEntries->nextEntry = ldap_first_entry( pEntries->ld,
                           pEntries->searchResult );
   else
      pEntries->nextEntry = ldap_next_entry( pEntries->ld, 
                           pEntries->lastEntry);
   
   if (pEntries->nextEntry)
      return UCXCreateBoolean(CP, TRUE, FN);
   else
      return UCXCreateBoolean(CP, FALSE, FN);
}
   
UCX_API UCXLDAPEntryLibProc(void *CP, void *params, CHAR_PTR FN, uint32 Event,
   UCXClassLink *ClassLink, void *Object)
{
   switch( Event )
   {
   case UCX_EVENT_OBJECT_CREATE:
      return UCXCreateBoolean(CP, TRUE, FN);
   
   case  UCX_EVENT_PROPERTY_SET:
      if ( (stricmp((char const *)FN,"FULLNAME") == 0)){
         UCXSetErrorText(CP, -300, FN, "Property is Readonly");
         return NULL;
      }
      break;
   }
   // Dispatch Events that are not used
   return(UCXDispatchEvent(CP, params, FN, Event, ClassLink, Object));
}

The Final Steps

Compile the source code and link it with relevant libraries to build an NLM.

Copy the NLM to the NetWare server and edit the SYS:UCS\UCX.INI file to set the path for the NLM. For example, UCX:LDAP=SYS:UCS\UCX\LDAP.NLM.

Component Usage

The following NSN script demonstrates the usage of this component to list the entries from an LDAP directory.

'Create an LDAP object, set the address and search for all the objects
'
Set ldap=createobject("UCX:LDAP")
container="ou=msrivathsa,ou=user,o=novell"
ldap.server="nldap.com"
ldap.port=389
ldap.login ("cn=admin,ou=msrivathsa,ou=user,o=novell", "novell")
Set entries=ldap.search("(objectclass=*)", container, 1)
Print "Total Entries Found: " &CStr(entries.count)
For Each entry In entries
   Print entry.fullname
Next
ldap.logout

Conclusion

The scripting infrastructure on NetWare helps in easy application development. It can be extended by developing UCX components for various services. UCX component is an encapsulated and abstracted unit of functionality, which exposes services in the form of properties, methods, and events. UCX components can be created using UCX SDK which is part of Novell Script for NetWare.

* Originally published in Novell AppNotes


Disclaimer

The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© Copyright Micro Focus or one of its affiliates