#!/usr/bin/env python # coding: utf-8 # # Intersight and UCS # # Exam Topics Covered: # 3.4 Construct API calls to retrieve data from Intersight # 3.5 Construct a Python script using the UCS APIs to provision a new UCS server given a template # # ## Intersight # Cisco Intersight has a modern REST API based on the [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md). The API documentation is located here: https://intersight.com/apidocs # # ### Authentication # 1) User must generate and download an RSA private key from the Intersight dashboard # # 2) An API key ID is also generated from the dashboard # # 3) Each request must be signed with the ID + the private key when communicating with Intersight # # # ![image.png](attachment:image.png) # # The Python requests library can't do this on its own. The code to do this is actually quite complex and probably beyond the scope of the exam. An example of what is required to handle the authentication is here: https://github.com/movinalot/intersight-rest-api/blob/master/intersight_auth.py. Another option is to use the SDK which handles the authentication behind the scenes. # # ### Retrieving Information from Intersight # The API supports GET, POST, PATCH and DELETE methods. With some help from the intersight-auth module above, the Python requests module can be used to interact with the API. Alternatively, the [Intersight SDK](https://github.com/CiscoUcs/intersight-python.git) can also be utilized. # # Below is a simple script leveraging the Python requests module and the intersight_auth module to handle the authentication and generate some REST calls to collect details from Intersight. # # # # In[ ]: import requests from ucs.intersight_auth import IntersightAuth api_key_id = "5eaf2f437564612d309e379a/5eaf2f437564612d309e37a7/5ef564117564612d33d47ce1" key_file = "SecretKey.txt" auth = IntersightAuth(secret_key_filename=key_file, api_key_id=api_key_id) base_url = "https://intersight.com/api/v1" # Blades resp = requests.get(f"{base_url}/compute/PhysicalSummaries", auth=auth) # List organizations resp = requests.get(f"{base_url}/organization/Organizations", auth=auth) # Service Profiles resp = requests.get(f"{base_url}/ls/ServiceProfiles", auth=auth) # Intersight also has an SDK which can be downloaded from https://github.com/CiscoUcs/intersight-python.git. The code below provides examples of using the SDK to retrieve data from Intersight. An instance of the IntersightApiClient class is first created, passing in the private key file and the API key. # # ``` # api_instance = IntersightApiClient( # "https://intersight.com/api/v1", # private_key="SecretKey.txt", # api_key_id = "5eaf2f437564612d309e379a/5eaf2f437564612d309e37a7/5ef564117564612d33d47ce1" # ) # ``` # # Then in order to retrieve information you can create an instance of one of the API classes, for example LsServiceProfileApi, passing the IntersightApiClient instance as an argument. # # ``` # sp_handle = ls_service_profile_api.LsServiceProfileApi(api_instance) # # type(sp_handle) # # ``` # This creates an instance of the LsServiceProfileApi class. You can then call one of the available methods of the class to get, update, or delete Service Profiles in Intersight. # # ``` # sp_handle.ls_service_profiles_get() # {'count': None, # 'results': [{'account_moid': '5eaf2f437564612d309e379a', # 'ancestors': [], # 'assign_state': 'unassigned', # 'assoc_state': 'unassociated', # 'associated_server': '', # 'config_state': 'not-applied', # 'create_time': datetime.datetime(2020, 6, 17, 21, 3, 11, 779000, tzinfo=tzutc()), # 'device_mo_id': '5eea80b06f72612d31ec82c7', # 'dn': 'org-root/ls-test', # 'domain_group_moid': '5eaf2f437564612d309e37a2', # 'mod_time': datetime.datetime(2020, 6, 17, 21, 3, 11, 824000, tzinfo=tzutc()), # 'moid': '5eea850f6176752d3267de4e', # 'name': 'test', # 'object_type': 'ls.ServiceProfile', # 'oper_state': 'unassociated', # 'owners': ['5eaf2f437564612d309e379a', # '5eea80b06f72612d31ec82c7'], # 'parent': None, # 'permission_resources': [{'moid': '5eaf2f466972652d327449da', # 'object_type': 'organization.Organization', # 'selector': None}], # 'registered_device': {'moid': '5eea80b06f72612d31ec82c7', # 'object_type': 'asset.DeviceRegistration', # 'selector': None}, # 'rn': '', # 'shared_scope': '', # 'tags': [], # 'version_context': None}]} # ``` # # To create an object, you would call the POST method of the instance and pass in the appropriate data as a dictionary. Below is an example of creating an organization. # # ``` # org_handle = organization_organization_api.OrganizationOrganizationApi(api_instance) # # payload = { # "Description":"Test Org 2", # "Name": "Test2" # } # # org_handle.organization_organizations_post(body=payload) # ``` # # The code below shows several examples of getting and creating objects using the Intersight API. # In[ ]: from intersight.intersight_api_client import IntersightApiClient from intersight.apis import fault_instance_api from intersight.apis import inventory_device_info_api from intersight.apis import equipment_device_summary_api from intersight.apis import organization_organization_api from intersight.apis import ls_service_profile_api os.chdir(os.path.join(os.getcwd(), 'ucs')) try: # Create Intersight API Instance api_instance = IntersightApiClient( "https://intersight.com/api/v1", private_key="SecretKey.txt", # This file contains the Secret Key that was downloaded when the API Key was created api_key_id = "5eaf2f437564612d309e379a/5eaf2f437564612d309e37a7/5ee95fa37564612d3344d916" ) # Get Faults handle = fault_instance_api.FaultInstanceApi(api_instance) response = handle.fault_instances_get() except Exception as err: print("There was an error!") print("Status:", str(err.status)) print("Reason:", str(err.reason)) device_handle = inventory_device_info_api.InventoryDeviceInfoApi(api_instance) devices = device_handle.inventory_device_infos_get() # Get Organizations (not Organizations in UCSM, these are seen under Settings, Access & Permissions in Intersight) org_handle = organization_organization_api.OrganizationOrganizationApi(api_instance) orgs = org_handle.organization_organizations_get() for org in orgs.to_dict()['results']: print(org['name']) # Get Devices device_handle = equipment_device_summary_api.EquipmentDeviceSummaryApi(api_instance) devices = chassis_handle.equipment_device_summaries_get() devices_dict = devices.to_dict() for item in devices_dict['results']: print(item['dn'], item['object_type'], item['model'], item['serial']) # Get Service Profiles sp_handle = ls_service_profile_api.LsServiceProfileApi(api_instance) sps = sp_handle.ls_service_profiles_get() for sp in sps.to_dict()['results']: print(sp['object_type'], sp['name']) # Create new organization payload = { "Description":"Test Org 2", "Name": "Test2" } org_handle.organization_organizations_post(body=payload) # ## UCS # # UCS implements an [XML-based API](https://www.cisco.com/c/en/us/td/docs/unified_computing/ucs/sw/api/b_ucs_api_book/b_ucs_api_book_chapter_01.html). It is unique from many of the other Cisco product APIs in that all API requests will be POST requests whether information is being retrieved, created, updated, or deleted. In all cases, an XML payload will be sent along with the POST request which will inform UCS what action to take. An XML document will be received in response. A useful utility for working with the UCS API is the Python xmltodict library, which converts XML to a Python dictionary. It is not a native Python library, so it must be first installed with `pip install xmltodict`. There is also a useful UCSM SDK which abstracts away having to deal with building and processing XML documents. It can be installed with `pip install ucsmsdk`. # # > Press Ctrl-Option-Q (Mac) or Ctrl-Alt-Q (Windows) to enable the “Record XML” option in the UCS GUI. This will record what you are doing in the GUI and then provide the XML needed for the task. When the task has been completed in the GUI, click Stop XML Recording and it will prompt for a filename. The file will then be downloaded to your system. # # ### Authentication # Authentication is performed by sending an XML document in the format: # `` # # The response will be an XML document which will contain a cookie that must be included in all subsequent requests. # # ``` # url = 'https://10.10.20.113/nuova' # username = 'ucspe' # password = 'ucspe' # # login_body = f'' # # headers = {"Content-Type": "application/x-www-form-urlencoded"} # # # Log in # resp = requests.post(url=url, headers=headers, data=login_body, verify=False) # ``` # # Login response: # ``` # # ``` # Converted to JSON: # ``` # '{"aaaLogin": {"@cookie": "", "@response": "yes", "@outCookie": ' # '"1593347478/7bdd8f29-7dbc-42e4-b088-f59ac09e8488", "@outRefreshPeriod": ' # '"600", "@outPriv": ' # '"aaa,admin,ext-lan-config,ext-lan-policy,ext-lan-qos,ext-lan-security,ext-san-config,ext-san-policy,ext-san-security,fault,operations,pod-config,pod-policy,pod-qos,pod-security,read-only", ' # '"@outDomains": "org-root", "@outChannel": "noencssl", "@outEvtChannel": ' # '"noencssl", "@outSessionId": "", "@outVersion": "4.1(2c)", "@outName": ""}} # ``` # The @outCookie must be stored and used in all subsequent requests. # # # ### Querying Objects # A query can be made based on class or Distinguished Name of an object. There are also a number of query filters that can be applied. An XML document must be built and sent in a POST request. The XML document must contain the previously obtained cookie # # Below is an example query for the lsServer class. The `ls` means Logical Server, meaning Service Profiles. The query will return any configured Service Profiles. # ``` # query_sp = "" # # resp = requests.post(url=url, headers=headers, data=query_sp, verify=False) # # ``` # # If you have the DN of the object, you can query for it using the configResolveDn query: # ``` # query = """""" # # resp = requests.post(url=url, headers=headers, data=query, verify=False) # ``` # # Additional query types and filters can be found here: https://www.cisco.com/c/en/us/td/docs/unified_computing/ucs/sw/api/b_ucs_api_book/b_ucs_api_book_chapter_01.html?referring_site=RE&pos=1&page=https://www.cisco.com/c/en/us/td/docs/unified_computing/ucs/sw/api/b_ucs_api_book/b_ucs_api_book_chapter_00.html#r_usingconfigfinddnsbyclassid # # # # ### Creating a Service Profile # Creating a Service Profile is accomplished by creating an XML document containing the parameters of the Service Profile and then posting the document to the XML API. Below is an XML document and post request to create a Service Profile from a Service Profile template called `test`. The new Service Profile will be created from the template with the prefix `SP`, so the new Service Profile in UCSM should be `SP-test`. # # ``` # sp_xml = f""" # # """.strip() # # resp = requests.post(url=url, headers=headers, data=sp_xml, verify=False) # ``` # # Below is a full code example using the Python requests library to create a Service Profile from a template in UCS. # In[ ]: import requests import xmltodict url = 'https://10.10.20.113/nuova' username = 'ucspe' password = 'ucspe' login_body = f'' headers = {"Content-Type": "application/x-www-form-urlencoded"} # Log in resp = requests.post(url=url, headers=headers, data=login_body, verify=False) # Convert response to dict and eat the cookie content = xmltodict.parse(resp.content) cookie = content['aaaLogin']['@outCookie'] # Create the XML to post (This code launches an SP from an SP template called test) sp_xml = f""" """.strip() resp = requests.post(url=url, headers=headers, data=sp_xml, verify=False) # Note you will get a 200 OK even if it fails. Have to check the returned response for errorCode. resp_dict = xmltodict.parse(resp.content) if '@errorCode' not in resp_dict['lsInstantiateNTemplate']: print(f"Added SP with DN {resp_dict['lsInstantiateNTemplate']['outConfigs']['lsServer']['@dn']}" f" and ID {resp_dict['lsInstantiateNTemplate']['outConfigs']['lsServer']['@intId']}") else: print(resp_dict['lsInstantiateNTemplate']['@errorCode'], resp_dict['lsInstantiateNTemplate']['@errorDescr']) # > Note that some failures report a 200 OK HTTP status code, even though UCS encountered an error. For example, an attempt to create an object that already exists will return HTTP status code 200 with the error reported in the XML response as shown below. Due to this, it is necessary to check the response for an errorCode in the returned XML document. # # ``` # # # ``` # # > If a query for an object fails because it doesn't exist, UCS returns an XML document with an empty `` field. This is also an HTTP 200 OK status code, but with no errorCode. # # ``` # # # # # ``` # # Another advantage of UCSM SDK is that it takes care of handling the above errors for you. # ### UCSM SDK # # The UCSM SDK abstracts some of the lower level functions of forming and processing XML documents and error handling. It can be installed with `pip install ucsmsdk`. The documentation for the SDK is available here: https://ucsmsdk.readthedocs.io/en/latest/ # # #### Usage # The developer must first import and create a UcsHandle object, passing it the UCSM hostname, username and password as arguments. Then the `login` method is called to create an authenticated session. # # ``` # from ucsmsdk.ucshandle import UcsHandle # # hostname = '10.10.20.113' # username = 'ucspe' # password = 'ucspe' # # handle = UcsHandle(hostname, username, password) # handle.login() # ``` # From this point on the UcsHandle object can be used to interact with the API. There are methods of the UcsHandle class that are used to get, create, update, and delete objects in UCS: # # Retrieve an object - query_dn, query_classid, query_dns, query_classids # Create an object - add_mo # Update an object - set_mo # Delete an object - delete_mo # # Query an object: # ``` # blades = handle.query_classid("computeBlade") # ``` # This returns a list of ComputeBlade objects: # ``` # [, , , , , , , , ] # ``` # Each ComputeBlade object has a number of attributes that you can reference: # ``` # blades[0].model # 'UCSB-EX-M4-1' # ``` # # > Where to find the classes that can be queried upon. Two options: # - The Intersight API provides a useful API Reference section which provides a handy tool that allows you to search for classes as well as a Rest Client within the browser that will allow you to submit requests and see the results right in your browser. To get there, go to https://intersight.com/apidocs and click on the API Reference tab. The Rest Client will appear in the right hand pane when you click on a GET, POST, etc. request in the left hand pane. # - UCSPE has embedded Object Model documentation. To get there, click on Equipment in the lefthand pane from the initial login screen. Then click the question mark, and select API Model Documentation. There is also access to Visore, which allows you to query and display object model information. # # # Create an object: # ``` # from ucsmsdk.mometa.org.OrgOrg import OrgOrg # org = OrgOrg(parent_mo_or_dn="org-root", name="DevNet") # handle.add_mo(org) # handle.commit() # ``` # # > Note that nothing is submitted to the UCSM until the `commit()` method is executed # # Below is a functional code example of creating and managing objects using the UCSM SDK. # # In[1]: from ucsmsdk.ucshandle import UcsHandle hostname = '172.16.4.120' username = 'ucspe' password = 'ucspe' # Logging in handle = UcsHandle(hostname, username, password) handle.login() # Query examples if handle.is_valid(): blades = handle.query_classid("computeBlade") for blade in blades: print(blade) for blade in blades: print(blade.dn, blade.num_of_cpus, blade.available_memory) # Create a new Organization from ucsmsdk.mometa.org.OrgOrg import OrgOrg org = OrgOrg(parent_mo_or_dn="org-root", name="DevNet") handle.add_mo(org) handle.commit() # Create a new Service Profile from ucsmsdk.mometa.ls.LsServer import LsServer sp = LsServer(parent_mo_or_dn="org-root", name="devnet_sp") handle.add_mo(sp) handle.commit() # Update a Service Profile existing_sp = handle.query_dn("org-root/ls-devnet_sp") existing_sp.descr = 'SERVER01' existing_sp.usr_lbl = 'BIGBADSERVER' handle.set_mo(existing_sp) handle.commit() # Query Objects via ClassID orgs = handle.query_classid("orgOrg") # Query with filter (Returns a List containing matched objects) filter_str = '(name, "DevNet", type="eq")' org = handle.query_classid(class_id="OrgOrg", filter_str=filter_str) print(org[0].name) print(org[0].dn) # Deleting objects org = handle.query_dn("org-root/org-DevNet") handle.remove_mo(org) handle.commit() handle.logout() # In[ ]: