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()