Thursday, 5 April 2018

How to Deploy a UCS Manager Environment with Far Less Python Code

A co-worker sent me some infrastructure code to check out and get my opinion. The code was fine, but it was a lot of code… I mean a lot of code. Looking at the thousands of lines of purpose written code I started thinking that there could be a better way. The code was Python and used the UCS Python SDK to deploy a UCS Manager environment and it worked, it did everything it was supposed to do, but it did only those things.

I want to show you how to use the UCS Python SDK to

◈ Write less Python Code
◈ Use Python Dynamic Module loading
◈ Use Python Reflection to dynamically create UCS Object instances
◈ Make your Python code do everything with respect to the UCS Object Model
◈ Allow your Python code to use the variants of the UCS Python SDK without changing the code

The Infrastructure as Code Grind


Programming infrastructure can become overwhelming with the multitude of things that need to be created and updated. Gone are the days of submitting a request for a dev/test/prod environment where a human intercedes and does something manually.

Hold on!,… Are those manual days really gone? There is a human somewhere actually doing something manually for Infrastructure as Code. They are writing the code. Surprise! Perhaps you already knew that. Almost every organization, enterprise, and industry IT departments are writing code to automate delivery of infrastructure, and they are writing a lot of it. Literally grinding it out, with a lot of the code being very similar in purpose.

Similar, but not the Same


Sometimes I feel like I’m writing a lot of similar code. Similar code, but not similar enough that functionalization isn’t a bit messy or where cut-and-paste works all that well (usually introduces more errors than if you had just written the code).

When programming it seems that we create a lot of similar code to do the similar things over and over again. For example, if you are writing a command line utility you might reuse bits of code to process similar command line arguments. The code to process command line arguments from one command line utility will be similar to another… but not the same.

Cisco UCS Object Model Object Similarities


The Cisco UCS Object Model is a representation of all UCS objects logical or physical. The objects are all very similar, so much so that the code that makes up the UCS Python SDK and the UCS PowerTool Suite are ninety-nine percent generated. 99% is a lot of similarities but you would never mistake a VLAN object for a Service Profile object, or a Boot Policy object for a MAC Pool object. The similarities come in the form of structure, properties, privileges, processing, etc. These similarities and the generated UCS Python SDK code that I am going to focus on and show you how to write less code.

Python Reflection and Dynamic Module Loading Lets You Write Much Less Code


Python Reflection or Introspection is a way to manage your program’s current state. Whether it’s an object, function, module, class, etc. Python’s reflection-enabling functions like getattr(), setattr(), type(), isinstance(), callable(), etc. provide insight and control. Combine those functions with Python’s dynamic module loading function import_module() and you now have the ability to write less code, much less code.

Let that sink in for a second, swirl it around in your glass, take a deep breath in… now check this out.

Cisco UCS Python SDK Object Specific Code


Mostly we write code that is object-specific. For example, if you want to add a VLAN to a UCS Manager using the UCS Python SDK (link) you need to:

◈ Query for the FabricLanCloud so you know where to put the new VLAN
◈ Import the FabricVlan module
◈ instantiate an instance of the FabricVlan class, minimally setting these attributes
    ◈ id
    ◈ name
◈ Add the VLAN object instance to the UCS connection handle
◈ Commit the handle

If all went well the new VLAN is added to the Fabric LAN Cloud. The code below shows this process including the UCS handle creation code.

from ucsmsdk.ucshandle import UcsHandle
from ucsmsdk.mometa.fabric.FabricVlan import FabricVlan

handle = UcsHandle("192.168.220.201", "admin", "password")
handle.login()

fabric_lan_cloud = handle.query_dn("fabric/lan")
vlan_100 = FabricVlan(parent_mo_or_dn=fabric_lan_cloud,
           name="vlan100", 
           id="100")

handle.add_mo(vlan_100)
handle.commit()

handle.logout()

There is nothing wrong with this code. It could be enhanced with exception handling and parameterization of username, password, UCS IP, VLAN ID, and VLAN Name. You could also add validation of those parameters as well as some command line options for the parameters. Then, after you did all that, you would have a Python program that adds a VLAN to UCS Manager. Considering there are thousands and thousands of UCS Manager objects you better stock up on whatever fuels your development sessions.

Dynamically Adjusting Python Code


Python Reflection enables you to write dynamically adjusting code. Code that reads object configuration from a file, perhaps for a VLAN or ANY other object or objects and then creates or updates those objects.

The Python code below, regardless of the object, will not need to be changed from the original code.  The configuration file instructs the Python code which module to import, which class to instantiate and which attributes of the object to set or update.

This is code for a VLAN object… actually this is the code for any and all of the thousands and thousands of UCS objects:

from importlib import import_module
import json
import logging
import os
import sys
import yaml

logging.basicConfig(level=logging.DEBUG, 
                    format='%(asctime)s - %(levelname)s - %(message)s')

def traverse(managed_object, mo=''):
    logging.info(managed_object['class'])
    logging.debug(managed_object['module'])
    logging.debug(managed_object['properties'])

    mo_module = import_module(managed_object['module'])
    mo_class = getattr(mo_module, managed_object['class'])

    if 'parent_mo_or_dn' not in managed_object['properties']:
        managed_object['properties']['parent_mo_or_dn'] = mo

    mo = mo_class(**managed_object['properties'])
    logging.debug(mo)

    handle.add_mo(mo, modify_present = True)

    if 'children' in managed_object:
        for child in managed_object['children']:
            traverse(child, mo)

if __name__ == '__main__':

    filename = os.path.join(sys.path[0], sys.argv[1])

    logging.info('Reading config file: ' + filename)
    try:
        with open(filename, 'r') as file:
            if filename.endswith('.json'):
                config = json.load(file)
            elif filename.endswith('.yml'):
                config = yaml.load(file)
            else:
                logging.info('Unsupported file extension for configuration file: ' + filename)
    
    except IOError as eError:
        sys.exit(eError)

    mo_module = import_module(config['connection']['module'])
    obj_class = config['connection']['class']
    mo_class = getattr(mo_module, obj_class)

    handle = mo_class(**config['connection']['properties'])
    handle.login()

    for managed_object in config['objects']:
        traverse(managed_object)
        if config['connection']['commit-buffer']:
            handle.commit()

    handle.logout()

These approximately thirty lines of actual code will work for every UCS Object Model object.

The Configuration File Is What Drives the Code


The configuration file shown below has two main sections:

◈ connection – indicates the connection information
◈ objects – the objects to be created

{
    "connection":
    {
        "module":"ucsmsdk.ucshandle",
        "class":"UcsHandle",
        "commit-buffer": true,
        "properties":{
            "ip":"10.10.10.10",
            "username":"admin",
            "password":"password",
            "secure":true
        }
    },
    "objects": [
        {
            "module": "ucsmsdk.mometa.fabric.FabricLanCloud",
            "class": "FabricLanCloud",
            "properties":{
                "parent_mo_or_dn": "fabric"
            },
            "message": "add vlans",
            "children": [
                {
                    "module": "ucsmsdk.mometa.fabric.FabricVlan",
                    "class": "FabricVlan",
                    "properties":{
                        "id": "700",
                        "name": "vlan700"
                    },
                    "message": "add vlan 700"
                },{
                    "module": "ucsmsdk.mometa.fabric.FabricVlan",
                    "class": "FabricVlan",
                    "properties":{
                        "id": "701",
                        "name": "vlan701"
                    },
                    "message": "add vlan 701"
                }
            ]
        },{
            "module": "ucsmsdk.mometa.org.OrgOrg",
            "class": "OrgOrg",
            "properties":{
                "parent_mo_or_dn": "org-root",
                "name": "prod-west"
            },
            "message": "created organization prod-west"
        },{
            "module": "ucsmsdk.mometa.org.OrgOrg",
    "class": "OrgOrg",
    "properties":{
                "parent_mo_or_dn": "org-root",
    "name": "prod-east"
            },
            "message": "create organization prod-east",
"children": [
                {
                    "module": "ucsmsdk.mometa.org.OrgOrg",
        "class": "OrgOrg",
        "properties":{
        "name": "DC01"
                    },
                    "message": "created organization prod-east/DC01"
                },{
                    "module": "ucsmsdk.mometa.org.OrgOrg",
                    "class": "OrgOrg",
                    "properties":{
                        "name": "DC02"
                    },
                    "message": "created organization prod-east/DC02"
                }
            ]
        }
    ]
}

JSON encoding is used in this case, but YAML or XML or anything that can be loaded into a Python dictionary will work.

The connection section indicates which UCS system to connect to. In the sample JSON a UCS Manager connection is specified. However, change the module “ucsmsdk.ucshandle” to “imcsdk.imchandle” and the code works with a Cisco Integrated Management Controller (CIMC) connection. The code was not changed, the configuration file was changed.

The objects section specifies each object to create and the object’s children (if the object has any children). As well, if the children objects have children those objects will be created or updated and so on until there are no more decedents.

Depth-First Search is the Key


The configuration file is loaded into a Python dictionary and is traversed using a depth-first search. The image depicts a depth-first search.

Cisco Tutorials and Materials, Cisco Learning, Cisco Guides, Cisco Python Code

Depth First Search Showing the Traversal of the Configuration File

With respect to the configuration file, in this image node 1 is the “objects” list. The second level nodes are parent level objects; node 2 is the Lan Cloud and node 5 is the root Organization. Under the Lan Cloud nodes 3 and 4 are VLANs “700” and “701”. Nodes 6 and 8 are sub-organizations of the root organization; “prod-west” and “prod-east”. Finally, nodes 7 and 9 are sub-organizations of the prod-east organization; “DC01” and “DC02”

Every piece of information needed to create or update a UCS Managed Object is in the configuration file – the module, the class, the parent object, the children objects and the properties for each of those objects.

The children objects only vary from the initial parent object in that their parent object is not encoded in the configuration file but inherited from the enclosing parent object.

As you can see from the code above, with all the required module, class, and attribute information in the configuration file along with the hierarchical object structure these few lines of code can create any UCS object.

The additional plus with the configuration file driven code is that new and/or updated objects are added to the configuration file or put in their own configuration file. No new code needs to be written.

The UCS Python SDK was built to work this way. The uniformity of the object model enables these capabilities for reflection-based programming.

What about CI/CD and Metadata


You’re probably thinking, “could it Get Any Better?” The answer… It can!

How about maintaining your configurations in a source repository and use CI/CD methodologies to really treat your infrastructure as code.

Or use the UCS Python SDK built-in object metadata to validate the configuration file prior to processing.

The metadata blog will cover the UCS Python SDK built-in metadata, specifically

◈ How to Access the UCS Python SDK Metadata
◈ How to Generate UCS Managed Object Metadata
◈ How Use UCS Python SDK Metadata Restrictions to Validate Input
   ◈ Patterns
   ◈ Enumerations
   ◈ Ranges

DevNet Resources and Python Code


I want to get you started, so here’s the code, also helpful are the following DevNet resources (all free, of course, just sign up, that’s free too)

◈ DevNet Data Center – APIs, Code Samples, Blogs, etc.
◈ DevNet UCS Management Learning Labs – Get spun up on UCS Management
◈ DevNet Sandboxes for UCS Management – Spin up a virtual or physical UCS environment
◈ UCS Management
◈ Cisco Integrated Management Controller

For an in-depth look at the code and configuration file watch the video.

Related Posts

0 comments:

Post a Comment