Skip to content

Commit

Permalink
Merge pull request #246 from aristanetworks/release-1.3.0
Browse files Browse the repository at this point in the history
Release 1.3.0
  • Loading branch information
mharista authored Mar 7, 2023
2 parents c1981db + f56882f commit 39c258f
Show file tree
Hide file tree
Showing 21 changed files with 1,829 additions and 44 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.2
1.3.0
2 changes: 1 addition & 1 deletion cvprac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@
''' RESTful API Client class for Cloudvision(R) Portal
'''

__version__ = '1.2.2'
__version__ = '1.3.0'
__author__ = 'Arista Networks, Inc.'
89 changes: 79 additions & 10 deletions cvprac/cvp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2739,14 +2739,23 @@ def delete_change_controls(self, cc_ids):
Args:
cc_ids (list): A list of change control IDs to be deleted.
'''
if self.clnt.apiversion is None:
self.get_cvp_info()
if self.clnt.apiversion >= 3.0:
msg = 'Change Control Resource APIs supported from 2021.2.0 or newer.'
if self.cvp_version_compare('>=', 6.0, msg):
self.log.debug(
'v6+ Using Resource API Change Control Delete API Call')
resp_list = []
for cc_id in cc_ids:
resp = self.change_control_delete(cc_id)
resp_list.append(resp)
return resp_list

msg = 'Change Control Service APIs supported from 2019.0.0 to 2021.2.0'
if self.cvp_version_compare('>=', 3.0, msg):
self.log.debug(
'v3/v4/v5 /api/v3/services/ccapi.ChangeControl/Delete'
' API Call')
resp_list = []
for cc_id in cc_ids:
resp_list = []
data = {'cc_id': cc_id}
resp = self.clnt.post(
'/api/v3/services/ccapi.ChangeControl/Delete',
Expand Down Expand Up @@ -2870,7 +2879,6 @@ def reset_device(self, app_name, device, create_task=True):
Args:
app_name (str): String to specify info/signifier of calling app
device (dict): Device info
container (dict): Container info
create_task (bool): Determines whether or not to execute a save
and create the tasks (if any)
Expand All @@ -2880,14 +2888,19 @@ def reset_device(self, app_name, device, create_task=True):
Ex: {u'data': {u'status': u'success', u'taskIds': []}}
'''
info = ('App %s reseting device %s and moving it to Undefined'
info = ('App %s resetting device %s and moving it to Undefined'
% (app_name, device['fqdn']))
self.log.debug(info)

if 'parentContainerId' in device:
from_id = device['parentContainerId']
else:
parent_cont = self.get_parent_container_for_device(device['key'])
from_id = parent_cont['key']
if parent_cont and 'key' in parent_cont:
from_id = parent_cont['key']
else:
from_id = ''

data = {'data': [{'info': info,
'infoPreview': info,
'action': 'reset',
Expand Down Expand Up @@ -3781,16 +3794,72 @@ def device_decommissioning_status_get_all(self, status="DECOMMISSIONING_STATUS_U
self.log.debug('v7 ' + str(url))
return self.clnt.post(url, data=payload, timeout=self.request_timeout)

def add_role(self, name, description, moduleList):
''' Add new local role to the CVP UI.
Args:
name (str): local role name on CVP
description (str): role description
moduleList (list): list of modules (name (str) and mode (str))
'''
data = {"name": name,
"description": description,
"moduleList": moduleList}
return self.clnt.post('/role/createRole.do', data=data,
timeout=self.request_timeout)

def update_role(self, rolekey, name, description, moduleList):
''' Updates role information, like
role name, description and role modules.
Args:
rolekey (str): local role key on CVP
name (str): local role name on CVP
description (str): role description
moduleList (list): list of modules (name (str) and mode (str))
'''
data = {"key": rolekey,
"name": name,
"description": description,
"moduleList": moduleList}
return self.clnt.post('/role/updateRole.do', data=data,
timeout=self.request_timeout)

def get_role(self, rolekey):
''' Returns specified role information.
Args:
rolekey (str): role key on CVP
Returns:
response (dict): Returns a dict that contains the role.
Ex: {'name': 'Test Role', 'key': 'role_1599019487020581247', 'description': 'Test'...}
'''
return self.clnt.get('/role/getRole.do?roleId={}'.format(rolekey),
timeout=self.request_timeout)

def get_roles(self):
''' Get all the user roles in CloudVision.
Returns:
response (dict): Returns a dict that contains all the user role states..
Ex: {'total': 7, 'roles': [{'name': 'Test Role', 'key': 'role_1599019487020581247',
'description': 'Test'...}
'description': 'Test'...}]}
'''
url = '/role/getRoles.do?startIndex=0&endIndex=0'
return self.clnt.get(url, timeout=self.request_timeout)

def delete_role(self, rolekey):
''' Remove specified role from CVP
Args:
rolekey (str): role key on CVP
'''
data = [rolekey]
return self.delete_roles(data)

def delete_roles(self, rolekeys):
''' Remove specified roles from CVP
Args:
rolekeys (list): list of role keys (str) on CVP
'''
return self.clnt.post('/role/deleteRoles.do', data=rolekeys,
timeout=self.request_timeout)

def svc_account_token_get_all(self):
''' Get all service account token states using Resource APIs.
Supported versions: CVP 2021.3.0 or newer and CVaaS.
Expand Down Expand Up @@ -3877,7 +3946,7 @@ def svc_account_get_all(self):
'''
msg = 'Service Account Resource APIs are supported from 2021.3.0+.'
if self.cvp_version_compare('>=', 7.0, msg):
url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetAll'
url = '/api/v3/services/arista.serviceaccount.v1.AccountService/GetAll'
self.log.debug('v7 {} '.format(url))
return self.clnt.post(url)

Expand All @@ -3895,7 +3964,7 @@ def svc_account_get_one(self, username):
msg = 'Service Account Resource APIs are supported from 2021.3.0+.'
if self.cvp_version_compare('>=', 7.0, msg):
payload = {"key": {"name": username}}
url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetOne'
url = '/api/v3/services/arista.serviceaccount.v1.AccountService/GetOne'
self.log.debug('v7 {} {}'.format(url, payload))
return self.clnt.post(url, data=payload)

Expand Down
19 changes: 15 additions & 4 deletions cvprac/cvp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def __init__(self, logger='cvprac', syslog=False, filename=None,
self.api_token = None
self.version = None
self._last_used_node = None
self.proxies = None

# Save proper headers
self.headers = {'Accept': 'application/json',
Expand Down Expand Up @@ -258,7 +259,8 @@ def set_version(self, version):

def connect(self, nodes, username, password, connect_timeout=10,
request_timeout=30, protocol='https', port=None, cert=False,
is_cvaas=False, tenant=None, api_token=None, cvaas_token=None):
is_cvaas=False, tenant=None, api_token=None, cvaas_token=None,
proxies=None):
''' Login to CVP and get a session ID and cookie. Currently
certificates are not verified if the https protocol is specified. A
warning may be printed out from the requests module for this case.
Expand Down Expand Up @@ -291,6 +293,14 @@ def connect(self, nodes, username, password, connect_timeout=10,
for CVaaS.
api_token (string): API Token to use in place of UN/PW login
for CVP 2020.3.0 and beyond.
proxies (dict): A dictionary of proxy protocol to URL. Example:
{'http': 'hostname.domain.com:8080',
'https': 'hostname.domain.com:8080'}
Proxies can also be set via environment variables.
Please reference the below link for details of precedence.
https://requests.readthedocs.io/en/latest/user/advanced/#proxies
Raises:
CvpLoginError: A CvpLoginError is raised if a connection
Expand Down Expand Up @@ -345,6 +355,7 @@ def connect(self, nodes, username, password, connect_timeout=10,
' generic')
self.api_token = api_token
self.cvaas_token = api_token
self.proxies = proxies
self._create_session(all_nodes=True)
# Verify that we can connect to at least one node
if not self.session:
Expand Down Expand Up @@ -379,6 +390,8 @@ def _reset_session(self):
be set to None.
'''
self.session = requests.Session()
if self.proxies:
self.session.proxies.update(self.proxies)
return_error = None
try:
self._login()
Expand Down Expand Up @@ -703,9 +716,7 @@ def _make_request(self, req_type, url, timeout, data=None,
else:
self.log.error('Unknown format for JSONDecodeError - %s',
err_str)
# Suppressing context as per
# https://peps.python.org/pep-0409/
raise JSONDecodeError(err_str) from None
raise error

def _send_request(self, req_type, full_url, timeout, data=None,
files=None):
Expand Down
36 changes: 36 additions & 0 deletions docs/labs/lab03-configlet-management/get_applied_netelements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (c) 2023 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the COPYING file.

from cvprac.cvp_client import CvpClient
import ssl
import argparse
ssl._create_default_https_context = ssl._create_unverified_context
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()

# Create connection to CloudVision
clnt = CvpClient()
clnt.connect(nodes=['cvp1'], username="username", password="password")

parser = argparse.ArgumentParser(
description='Get the list of devices and containers a configlet is attached to')
parser.add_argument('-c', '--configlet', required=True, help='The name of the configlet')
args = parser.parse_args()

configlet_name = args.configlet
devices = clnt.api.get_applied_devices(configlet_name)

containers = clnt.api.get_applied_containers(configlet_name)
print(f"Total number of devices {configlet_name} is attached to: {devices['total']}\n")
print(f"Total number of containers {configlet_name} is attached to: {containers['total']}\n")
col1 = "Device FQDN/hostname"
col2 = "IP Address"
print(f"{col1:<40}{col2:<40}")
print("="*80)
for device in devices['data']:
print(f"{device['hostName']:<40}{device['ipAddress']}")

print("\nList of containers:\n")
for container in containers['data']:
print(container['containerName'])
120 changes: 120 additions & 0 deletions docs/labs/lab06-provisioning/atd_e2e_provisioning_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright (c) 2021 Arista Networks, Inc.
# Use of this source code is governed by the Apache License 2.0
# that can be found in the COPYING file.

# This script is an example on provisioning registered devices in CloudVision that is based on
# Arista Test Drive (ATD) and similar to what the ansible playbooks do in
# https://github.com/arista-netdevops-community/atd-avd.
# It does the following:
# - creates and uploads configlets,
# - creates the container hierarchy in Network Provisiong
# - moves the devices to their target containers
# - assigns the configlets to the devices
# - creates a change control from the genereated tasks
# - approves and executes the change control

import uuid
import time
import ssl
from datetime import datetime
from cvprac.cvp_client import CvpClient
ssl._create_default_https_context = ssl._create_unverified_context

# Create connection to CloudVision
clnt = CvpClient()
clnt.connect(['cvp1'],'username', 'password')

# Create container topology
container_name = "DC1_LEAFS"
container_topology = [{"containerName": "ATD_FABRIC", "parentContainerName": 'Tenant'},
{"containerName": "ATD_LEAFS", "parentContainerName": 'ATD_FABRIC'},
{"containerName": "pod1", "parentContainerName": 'ATD_LEAFS'},
{"containerName": "pod2", "parentContainerName": 'ATD_LEAFS'},
{"containerName": "ATD_SERVERS", "parentContainerName": 'ATD_FABRIC'},
{"containerName": "ATD_SPINES", "parentContainerName": 'ATD_FABRIC'},
{"containerName": "ATD_TENANT_NETWORKS", "parentContainerName": 'ATD_FABRIC'}]
for container in container_topology:
try:
container_name = container['containerName']
# Get parent container information
parent = clnt.api.get_container_by_name(container['parentContainerName'])
print(f'Creating container {container_name}\n')
clnt.api.add_container(container_name,parent["name"],parent["key"])
except Exception as e:
if "Data already exists in Database" in str(e):
print ("Container already exists, continuing...")

# Create device mappers
devices = [{'deviceName': "leaf1",
'configlets': ["BaseIPv4_Leaf1", "AVD_leaf1"],
"parentContainerName": "pod1"},
{'deviceName': "leaf2",
'configlets': ["BaseIPv4_Leaf2", "AVD_leaf2"],
"parentContainerName": "pod1"},
{'deviceName': "leaf3",
'configlets': ["BaseIPv4_Leaf3", "AVD_leaf3"],
"parentContainerName": "pod2"},
{'deviceName': "leaf4",
'configlets': ["BaseIPv4_Leaf4", "AVD_leaf4"],
"parentContainerName": "pod2"},
{'deviceName': "spine1",
'configlets': ["BaseIPv4_Spine1", "AVD_spine1"],
"parentContainerName": "ATD_SPINES"},
{'deviceName': "spine2",
'configlets': ["BaseIPv4_Spine2", "AVD_spine2"],
"parentContainerName": "ATD_SPINES"}]

task_list = []
for device in devices:
# Load the AVD configlets from file
with open("./configlets/AVD_" + device['deviceName'] + ".cfg", "r") as file:
configlet_file = file.read()
avd_configlet_name = device['configlets'][1]
base_configlet_name = device['configlets'][0] # preloaded configlet in an ATD environment
container_name = device['parentContainerName']
base_configlet = clnt.api.get_configlet_by_name(base_configlet_name)
configlets = [base_configlet]
# Update the AVD configlets if they exist, otherwise upload them from the configlets folder
print (f"Creating configlet {avd_configlet_name} for {device['deviceName']}\n")
try:
configlet = clnt.api.get_configlet_by_name(avd_configlet_name)
clnt.api.update_configlet(configlet_file, configlet['key'], avd_configlet_name)
configlets.append(configlet)
except:
clnt.api.add_configlet(avd_configlet_name, configlet_file)
configlet = clnt.api.get_configlet_by_name(avd_configlet_name)
configlets.append(configlet)
# Get device data
device_data = clnt.api.get_device_by_name(device['deviceName'] + ".atd.lab")
# Get the parent container data for the device
container = clnt.api.get_container_by_name(container_name)
device_name = device['deviceName']
print(f"Moving device {device_name} to container {container_name}\n")
# The move action will create the task first, however if the devices are already in the target
# container, for instance if the script was run multiple times than the move action will
# not generate a task anymore, therefore it's better to create the task list from the
# Update Config action which will reuse the Move Device action's task if one exists,
# otherwise will create a new one.
move = clnt.api.move_device_to_container("python", device_data, container)
apply_configlets = clnt.api.apply_configlets_to_device("", device_data, configlets)
task_list = task_list + apply_configlets['data']['taskIds']

print(f"Generated task IDs are: {task_list}\n")

# Generate unique ID for the change control
cc_id = str(uuid.uuid4())
cc_name = f"Change_{datetime.now().strftime('%Y%m%d_%H%M%S')}"

print("Creating Change control with the list of tasks")
clnt.api.change_control_create_for_tasks(cc_id, cc_name, task_list, series=False)

print("Approving Change Control")
# adding a few seconds sleep to avoid small time diff between the local system and CVP
time.sleep(2)
approve_note = "Approving CC via cvprac"
clnt.api.change_control_approve(cc_id, notes=approve_note)

# Start the change control
print("Executing Change Control...")
start_note = "Start the CC via cvprac"
clnt.api.change_control_start(cc_id, notes=start_note)
Loading

0 comments on commit 39c258f

Please sign in to comment.