APIs

Overview

  • Most APIs are RESTful which are built on HTTP, using typical GET, POST, PUT, DELETE HTTP requests
    • GET - Pull data
    • POST - Create new data
    • PATCH - Replace part of data (only modified fields must be sent)
    • PUT - Replace entirety of data (all data for a resource will be overwritten)
    • DELETE - Delete data
  • Non-RESTful APIs typically use CLI commands instead of typical HTTP methods

NETCONF vs RESTCONF

  • NETCONF
    • Transport: SSH
    • Encoding: XML
    • Data model: YANG
    • Connection-oriented - must have open session with device
    • Can push configuration data to a candidate config and later commit
  • RESTCONF
    • Transport: HTTP
    • Encoding: JSON/XML (JSON preferred)
    • Data model: YANG
    • Subset of NETCONF
    • Not connection-oriented - actions performed individually
    • RESTful API designed specifically for administering network devices
    • Does not support a candidate configuration - changes made to running config

HTTP Authentication

  • Basic - Base64-encoded username and password values
  • Token-based - Token generated on API server and used for future API requests
  • Session-based - Session cookie generated on API server and used for future API requests

HTTP Status Codes

  • 1xx -> Informational
  • 2xx -> Success
  • 3xx -> Redirection
  • 4xx -> Client Error
    • Notables are 401 unauthorized, 403 forbidden, 404 resource not found
  • 5xx -> Server Error
    • Notables are 500 Internal server error, 503 service unavailable

Python API Interaction

Basic Authentication
# 'basic_auth' variable created by taking the username and password (cisco:cisco) as a string, converting it to a bytes string, encoding it to Base64 and then decoding it back to a regular string. This allows the string to be stored in the variable and used for HTTP basic authentication.
import base64
basic_auth = base64.b64encode(bytes(u'cisco:cisco', "utf-8")).decode()

# String stored and ready for use
import requests

# Formatting headers & specifying URL for HTTP request
HEADERS = {"Accept":"application/yang-data+json", "Authorization" : "Basic {}".format(basic_auth)}
URL = "https://csr1kv1/restconf/data/Cisco-IOS-XE-native:native/interface"

# Making get request and printing response boolean
response = requests.get(url=URL, headers=HEADERS, verify=False)
print(response.ok)
Token-based Authentication
# Post API call made to a specific URL to provide user:pass and receive token to be used in later calls
import requests
DNA_URL = "https://sandboxdnac.cisco.com"
HEADERS = {"content-type":"application/json"}
USER = "devnetuser"
PASS = "Cisco123!"
AUTH = (USER, PASS)

# Adding auth URI string to the end of basic DNA URL above
LOGIN_URL = DNA_URL + "/api/system/v1/auth/token"

# Making request for token
result = requests.post(url=LOGIN_URL, auth=AUTH, headers=HEADERS, verify=False)

# Storing returned token value in new variable (must use 'Token' as result is a dictionary)
TOKEN = result.json()['Token']

# Then token can be referenced in HTTP header (using dictionary variable from earlier). Which key will be used depends on the specific API server - for Cisco it's 'X-Auth-Token'
HEADERS['X-Auth-Token'] = TOKEN

# Another API call made and response boolean printed to confirm token is working
INVENTORY_URL = DNA_URL +"/dna/intent/api/v1/network-device"
response = requests.get(url=INVENTORY_URL, headers=HEADERS, verify=False)
print(response.ok)
Session-based Authentication
# Post API call made to a specific URL to provide user:pass and receive session ID to be used in later calls
import requests
APIC_URL = "https://sandboxapicdc.cisco.com"
USER = "admin"
PASS = "ciscopsdt"
AUTH_BODY = {
    "aaaUser": {
        "attributes": {"name": USER, "pwd": PASS}
    }
}

# Adding auth URI string to the end of basic APIC URL above
LOGIN_URL = APIC_URL + "/api/aaaLogin.json"

# Create session variable and make POST API call to recieve session cookie, then confirm response boolean and cookie
SESSION_VAR = requests.Session()
response = SESSION_VAR.post(url=LOGIN_URL, json=AUTH_BODY, verify=False)
print(response.ok)
print(response.cookies)

# You can now make further API calls without referencing the headers or cookies
TENANT_URL = APIC_URL + "/api/node/class/fvTenant.json"
response = SESSION_VAR.get(url=TENANT_URL, verify=False)
print(response.ok)

Displaying Router Serial, OS from API (Cisco Example)
import requests
import json

# Check for module dependencies:
not_installed_modules = []

try:
    from requests.auth import HTTPBasicAuth
except ImportError:
    not_installed_modules.append("requests")
try:
    import yaml
except ImportError:
    not_installed_modules.append("PyYAML")


if not_installed_modules:
    print("Please install following Python modules:")

    for module in not_installed_modules:
        print("  - {module}").format(module=module)

    exit(1)

requests.packages.urllib3.disable_warnings()

HTTP_HEADERS = {
    "Content-Type": "application/yang-data+json",
    "Accept": "application/yang-data+json",
}

HTTP_AUTH = HTTPBasicAuth("cisco", "cisco")

def get_inventory():
    inventory = {
        "csr1kv1": {"username": "cisco", "password": "cisco"},
        "csr1kv2": {"username": "cisco", "password": "cisco"},
    }

    return inventory

def get_serial_data(host):

    url = "https://{host}/restconf/data/Cisco-IOS-XE-native:native" "/license/udi/sn".format(host=host)
    response = requests.get(url=url, headers=HTTP_HEADERS, auth=HTTP_AUTH, verify=False)
    result = json.loads(response.text)
    serial_data = result["Cisco-IOS-XE-native:sn"]

    return serial_data

def get_version_data(host):

    url = "https://{host}/restconf/data/Cisco-IOS-XE-native:native" "/version".format(host=host)
    response = requests.get(url, headers=HTTP_HEADERS, auth=HTTP_AUTH, verify=False)
    result = response.json()
    version_data = result["Cisco-IOS-XE-native:version"]

    return version_data

def get_hostname(host):
    url = "https://{host}/restconf/data/Cisco-IOS-XE-native:native" "/hostname".format(
        host=host
    )
    response = requests.get(url, headers=HTTP_HEADERS, auth=HTTP_AUTH, verify=False)
    result = json.loads(response.text)
    hostname = result.get("Cisco-IOS-XE-native:hostname")

    return hostname

def structure_data():

    devices = {}
    net_inventory = get_inventory()
    for host in net_inventory:
        devices[host] = {}
        devices[host]["serial_number"] = get_serial_data(host)
        devices[host]["os_version"] = get_version_data(host)
        devices[host]["hostname"] = get_hostname(host)
    return devices

def main():

    structured_data = structure_data()

    heading = "| {:^10} | {:^10} | {:^15} | {:^12} |".format(
        "Device", "Hostname", "Serial Number", "OS Version"
    )
    print(len(heading) * "-")
    print(heading)
    print(len(heading) * "-")
    for hostname, details in structured_data.items():
        print(
            "| {:^10} | {:^10} | {:^15} | {:^12} |".format(
                hostname,
                details["hostname"],
                details["serial_number"],
                details["os_version"],
            )
        )
        print(len(heading) * "-")

if __name__ == "__main__":
    main()