Skip to content

Commit

Permalink
Feat: Implement image bundle on device level (#505)
Browse files Browse the repository at this point in the history
* Add image bundle info to cv_data

* Update schema for bundles

* Sketch in functions for applying image bundles

* WIP

* Implement apply or update image bundle to device

* Cleaup whitespace

* Implement bundle removal function

* Rename for consistency

* Fix API calls

* Enhanced logging

* Add results output

* enhanced logging

* More logging, fix serialNumber key

* Fix syntax

* Guard against empty results

* More debug logging

* Switch lookup to serial number

* Fix key mappings

* More logging

* check deviceelement

* issues with function

* Try DeviceElement thing

* Update DeviceElement for image_bundle

* Fix consts

* Add bundle attribute

* Update assignment with more logging

* More logging

* Update schema

* Tweak schema

* adjust field

* rollback changes

* update schema

* Handle device without assigned image bundle

* Handle bundle name is None

* More logging

* Update info

* Rename generic IMAGE_BUNDLE
Cleanup trailing whitespace
Explicitly set type as 'netelement' when assigining image

* Implement bundle detach

* Update documentation

* Handle image bundle not existing

* Fix whitespacing

* Fix whitespacing
Add manager for detach

* Fix python linting for logging

* Fix schema validation

* adjust schema

* adjust constant data

* Update constants

* bump indent to please PEP8

* Update description

* Add image bundle info to cv_data

* Update schema for bundles

* Sketch in functions for applying image bundles

* WIP

* Implement apply or update image bundle to device

* Cleaup whitespace

* Implement bundle removal function

* Rename for consistency

* Fix API calls

* Enhanced logging

* Add results output

* enhanced logging

* More logging, fix serialNumber key

* Fix syntax

* Guard against empty results

* More debug logging

* Switch lookup to serial number

* Fix key mappings

* More logging

* check deviceelement

* issues with function

* Try DeviceElement thing

* Update DeviceElement for image_bundle

* Fix consts

* Add bundle attribute

* Update assignment with more logging

* More logging

* Update schema

* Tweak schema

* adjust field

* rollback changes

* update schema

* Handle device without assigned image bundle

* Handle bundle name is None

* More logging

* Update info

* Rename generic IMAGE_BUNDLE
Cleanup trailing whitespace
Explicitly set type as 'netelement' when assigining image

* Implement bundle detach

* Update documentation

* Handle image bundle not existing

* Fix whitespacing

* Fix whitespacing
Add manager for detach

* Fix python linting for logging

* Fix schema validation

* adjust schema

* adjust constant data

* Update constants

* bump indent to please PEP8

* Update description

* Add example playbook

* Update comments

* Add empty line at EOF

* added remove image playbook

* Update to please the new markdown linter

* Update to fix markdown linter

* typo fix to make markdownlint happy

* facts_tools: change IMAGE_BUNDLE to IMAGE_BUNDLE_NAME as it was changed in the generic API

* Move example

* Revert "Update to please the new markdown linter"

This reverts commit 479a1bc.

* Revert "Update to fix markdown linter"

This reverts commit 000ba9e.

Co-authored-by: Colin MacGiollaEain <colin@flat-planet.net>
Co-authored-by: Sugetha Chandhrasekar <sugethakch@arista.com>
Co-authored-by: Tamas Plugor <tamas@arista.com>
  • Loading branch information
4 people authored Oct 13, 2022
1 parent 8aa7410 commit f07ebf7
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The following options may be specified for this module:
<td>loose</td>
<td><ul><li>loose</li><li>strict</li></ul></td>
<td>
<div>Set how configlets are attached/detached on device. If set to strict all configlets not listed in your vars are detached.</div>
<div>Set how configlets are attached/detached on device. If set to strict all configlets and image bundles not listed in your vars are detached.</div>
</td>
</tr>

Expand All @@ -47,7 +47,7 @@ The following options may be specified for this module:
<td></td>
<td></td>
<td>
<div>List of devices with their container and configlets information</div>
<div>List of devices with their container, configlet and image bundle information</div>
</td>
</tr>

Expand Down Expand Up @@ -91,6 +91,7 @@ The following options may be specified for this module:
parentContainerName: ANSIBLE
configlets:
- 'CV-EOS-ANSIBLE01'
imageBundle: leaf_image_bundle
tasks:
- name: "Configure devices on {{inventory_hostname}}"
arista.cvp.cv_device_v3:
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
- name: Device Image Management in Cloudvision
hosts: cv_server
connection: local
gather_facts: false
collections:
- arista.cvp
vars:
CVP_DEVICES:
- serialNumber: JPE504a004ea054
parentContainerName: L2_Leaf
configlets:
- 'AVD_Ipmi3'
imageBundle: leaf_bundle
tasks:
- name: "Configure devices on {{inventory_hostname}}"
arista.cvp.cv_device_v3:
devices: '{{CVP_DEVICES}}'
state: present
search_key: serialNumber
apply_mode: strict
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
- name: Device Image Management in Cloudvision
hosts: cv_server
connection: local
gather_facts: false
collections:
- arista.cvp
vars:
CVP_DEVICES:
- serialNumber: JPE504a004ea054
parentContainerName: L2_Leaf
configlets:
- 'AVD_Ipmi3'
tasks:
- name: "Configure devices on {{inventory_hostname}}"
arista.cvp.cv_device_v3:
devices: '{{CVP_DEVICES}}'
state: present
search_key: serialNumber
apply_mode: strict
231 changes: 228 additions & 3 deletions ansible_collections/arista/cvp/plugins/module_utils/device_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ def __init__(self, data: dict):
self.__serial = self.__data.get(Api.device.SERIAL)
self.__mgmtip = self.__data.get(Api.device.MGMTIP)
self.__container = self.__data[Api.generic.PARENT_CONTAINER_NAME]
# self.__image_bundle = [] # noqa # pylint: disable=unused-variable
self.__current_parent_container_id = None
# if Api.device.IMAGE_BUNDLE in self.__data:
# self.__image_bundle = data[Api.device.IMAGE_BUNDLE]
self.__image_bundle = self.__data.get(Api.device.BUNDLE)

if Api.device.BUNDLE in self.__data:
self.__image_bundle = data[Api.device.BUNDLE]
if Api.device.SYSMAC in data:
self.__sysmac = data[Api.device.SYSMAC]
if Api.device.SERIAL in data:
Expand Down Expand Up @@ -225,6 +226,18 @@ def parent_container_id(self):
"""
return self.__current_parent_container_id

@property
def image_bundle(self):
"""
image_bundle Getter for image bundle info
Returns
-------
dict
Dict with name, ID and type of bundle
"""
return self.__image_bundle

@parent_container_id.setter
def parent_container_id(self, id):
"""
Expand Down Expand Up @@ -263,6 +276,8 @@ def info(self):
else:
res[Api.generic.CONFIGLETS] = []
# res[Api.generic.PARENT_CONTAINER_ID] = self.__current_parent_container_id
if Api.generic.IMAGE_BUNDLE_NAME in self.__data:
res[Api.generic.IMAGE_BUNDLE_NAME] = self.__data[Api.generic.IMAGE_BUNDLE_NAME]
return res


Expand Down Expand Up @@ -420,6 +435,12 @@ def __get_device(self, search_value: str, search_by: str = Api.device.HOSTNAME):
cv_data = self.__cv_client.api.get_device_by_mac(device_mac=search_value)
elif search_by == Api.device.SERIAL:
cv_data = self.__cv_client.api.get_device_by_serial(device_serial=search_value)

if cv_data is not None and len(cv_data) > 0:
cv_data[Api.device.BUNDLE] = self.__cv_client.api.get_device_image_info(cv_data['key'])
else:
cv_data[Api.device.BUNDLE] = None

MODULE_LOGGER.debug('Got following data for %s using %s: %s', str(search_value), str(search_by), str(cv_data))
return cv_data

Expand Down Expand Up @@ -558,6 +579,8 @@ def __state_present(self, user_inventory: DeviceInventory, apply_mode: str = Mod
cv_move = CvManagerResult(builder_name=DeviceResponseFields.DEVICE_MOVED)
cv_configlets_attach = CvManagerResult(builder_name=DeviceResponseFields.CONFIGLET_ATTACHED)
cv_configlets_detach = CvManagerResult(builder_name=DeviceResponseFields.CONFIGLET_DETACHED)
cv_bundle_attach = CvManagerResult(builder_name=DeviceResponseFields.BUNDLE_ATTACHED)
cv_bundle_detach = CvManagerResult(builder_name=DeviceResponseFields.BUNDLE_DETACHED)
response = CvAnsibleResponse()

# Check if all devices are present on CV
Expand All @@ -584,6 +607,12 @@ def __state_present(self, user_inventory: DeviceInventory, apply_mode: str = Mod
for update in action_result:
cv_configlets_attach.add_change(change=update)

# Apply image bundle as set in inventory
action_result = self.apply_bundle(user_inventory=user_inventory)
if action_result is not None:
for update in action_result:
cv_bundle_attach.add_change(change=update)

# Remove configlets configured on CVP and if module runs in strict mode
if apply_mode == ModuleOptionValues.APPLY_MODE_STRICT:
action_result = self.detach_configlets(
Expand All @@ -592,15 +621,25 @@ def __state_present(self, user_inventory: DeviceInventory, apply_mode: str = Mod
for update in action_result:
cv_configlets_detach.add_change(change=update)

action_result = self.detach_bundle(
user_inventory=user_inventory)
if action_result is not None:
for update in action_result:
cv_bundle_detach.add_change(change=update)

# Generate result output
response.add_manager(cv_move)
MODULE_LOGGER.debug('AnsibleResponse updated, new content with cv_move: %s', str(response.content))
response.add_manager(cv_deploy)
MODULE_LOGGER.debug('AnsibleResponse updated, new content with cv_deploy: %s', str(response.content))
response.add_manager(cv_configlets_attach)
MODULE_LOGGER.debug('AnsibleResponse updated, new content with cv_configlets_attach: %s', str(response.content))
response.add_manager(cv_bundle_attach)
MODULE_LOGGER.debug('AnsibleResponse updated, new content with cv_bundle_attach: %s', str(response.content))
response.add_manager(cv_configlets_detach)
MODULE_LOGGER.debug('AnsibleResponse updated, new content with cv_configlets_detach: %s', str(response.content))
response.add_manager(cv_bundle_detach)
MODULE_LOGGER.debug('AnsibleResponse updated, new content with cv_bundle_detach: %s', str(response.content))

return response

Expand Down Expand Up @@ -887,6 +926,37 @@ def get_device_container(self, device_lookup: str):
}
return None

def get_device_image_bundle(self, device_lookup: str):
"""
get_device_image_bundle Retrieve image bundle attached to the device
Parameters
----------
device_lookup : str
Device name to look for
Returns
-------
dict
A dict with key and name
"""
cv_data = self.get_device_facts(device_lookup=device_lookup)
MODULE_LOGGER.debug('cv_data lookup returned: %s', str(cv_data))
if cv_data is not None and Api.generic.IMAGE_BUNDLE_NAME in cv_data:
if cv_data[Api.generic.IMAGE_BUNDLE_NAME][Api.image.NAME] is None:
return {
Api.generic.IMAGE_BUNDLE_NAME: None,
Api.image.ID: None,
Api.image.TYPE: None
}
else:
return {
Api.generic.IMAGE_BUNDLE_NAME: cv_data['imageBundle'][Api.image.NAME],
Api.image.ID: cv_data['imageBundle'][Api.image.ID],
Api.image.TYPE: cv_data['imageBundle']['imageBundleMapper'][cv_data['imageBundle'][Api.image.ID]][Api.image.TYPE]
}
return None

def get_container_info(self, container_name: str):
"""
get_container_info Retrieve container information from Cloudvision
Expand Down Expand Up @@ -1159,6 +1229,161 @@ def move_device(self, user_inventory: DeviceInventory):
results.append(result_data)
return results

def apply_bundle(self, user_inventory: DeviceInventory):
"""
apply_bundle - apply an image bundle to a device
Execute the API calls to attach an image bundle to a device.
Note that only 1 image bundle can be attached to a device.
If an image bundle is already attached to the device (type: netelement)
the new image bundle will replace the old.
Our behaviour is as follows;
* Bundle already attached to the device - update if different, skip if the same
* Bundle inherited from container (type: container) - attach bundle to device, regardless
of whether or not it is the same
Parameters
----------
user_inventory : DeviceInventory
Ansible inventory to configure on Cloudvision
Returns
-------
list
List of CvApiResult for all API calls
"""
results = []

for device in user_inventory.devices:
MODULE_LOGGER.debug("Applying image bundle for device: %s", str(device.fqdn))
result_data = CvApiResult(action_name=device.fqdn + '_image_bundle_attached')

# We can't attach/detach an image to a device in the undefined container
current_container_info = self.get_container_current(device_mac=device.system_mac)
if (device.configlets is None or current_container_info[Api.generic.NAME] == Api.container.UNDEFINED_CONTAINER_ID):
continue

# GET IMAGE BUNDLE
MODULE_LOGGER.debug("Get image bundle for %s", str(device.serial_number))
current_image_bundle = self.get_device_image_bundle(device_lookup=device.serial_number)
MODULE_LOGGER.debug("Current image bundle assigned is: %s", str(current_image_bundle))
MODULE_LOGGER.debug("User assigned image bundle is: %s", str(device.image_bundle))

if device.image_bundle is not None:
if device.image_bundle == current_image_bundle[Api.generic.IMAGE_BUNDLE_NAME] \
and current_image_bundle[Api.image.TYPE] == 'netelement':
MODULE_LOGGER.debug("No actions needed for device: %s", str(device.fqdn))
MODULE_LOGGER.debug("%s has %s assigned and applied", str(device.fqdn), current_image_bundle[Api.generic.IMAGE_BUNDLE_NAME])
pass
# Nothing to do
else:
MODULE_LOGGER.debug("Updating %s to use image bundle: %s", str(device.fqdn), str(device.image_bundle))

device_facts = {}
if self.__search_by == Api.device.FQDN:
device_facts = self.__cv_client.api.get_device_by_name(
fqdn=device.fqdn, search_by_hostname=False)
elif self.__search_by == Api.device.HOSTNAME:
device_facts = self.__cv_client.api.get_device_by_name(
fqdn=device.fqdn, search_by_hostname=True)
elif self.__search_by == Api.device.SERIAL:
device_facts = self.__cv_client.api.get_device_by_serial(device_serial=device.serial_number)

assigned_image_facts = self.__cv_client.api.get_image_bundle_by_name(device.image_bundle)
if assigned_image_facts is None:
MODULE_LOGGER.error('Error image bundle %s not found', str(device.image_bundle))
self.__ansible.fail_json(msg='Error applying bundle to device' + device.fqdn + ': ' + str(device.image_bundle) + 'not found')

MODULE_LOGGER.debug("%s image bundle facts are: %s", str(device.image_bundle), str(assigned_image_facts))
try:
resp = self.__cv_client.api.apply_image_to_element(
assigned_image_facts,
device_facts,
device.hostname,
'netelement'
)

except CvpApiError as catch_error:
MODULE_LOGGER.error('Error applying bundle to device: %s', str(catch_error))
self.__ansible.fail_json(msg='Error applying bundle to device' + device.fqdn + ': ' + catch_error)
else:
if resp['data']['status'] == 'success':
result_data.changed = True
result_data.success = True
result_data.taskIds = resp['data'][Api.task.TASK_IDS]

results.append(result_data)

return results

def detach_bundle(self, user_inventory: DeviceInventory):
"""
detach_bundle - detach an image bundle from a device
Execute the API calls to detach an image bundle from a device.
Parameters
----------
user_inventory : DeviceInventory
Ansible inventory to configure on Cloudvision
Returns
-------
list
List of CvApiResult for all API calls
"""
results = []

for device in user_inventory.devices:
MODULE_LOGGER.debug("Detaching image bundle from device: %s", str(device.fqdn))
result_data = CvApiResult(action_name=device.fqdn + '_image_bundle_detached')

# We can't attach/detach an image to a device in the undefined container
current_container_info = self.get_container_current(device_mac=device.system_mac)
if (device.configlets is None or current_container_info[Api.generic.NAME] == Api.container.UNDEFINED_CONTAINER_ID):
continue

# GET IMAGE BUNDLE
current_image_bundle = self.get_device_image_bundle(device_lookup=device.serial_number)

# Check to make sure that the assigned image isn't inherited from the container
if current_image_bundle is not None and current_image_bundle[Api.image.TYPE] == 'netelement':
if device.image_bundle is None:

device_facts = {}
if self.__search_by == Api.device.FQDN:
device_facts = self.__cv_client.api.get_device_by_name(
fqdn=device.fqdn, search_by_hostname=False)
elif self.__search_by == Api.device.HOSTNAME:
device_facts = self.__cv_client.api.get_device_by_name(
fqdn=device.fqdn, search_by_hostname=True)
elif self.__search_by == Api.device.SERIAL:
device_facts = self.__cv_client.api.get_device_by_serial(device_serial=device.serial_number)

assigned_image_facts = self.__cv_client.api.get_image_bundle_by_name(current_image_bundle[Api.generic.IMAGE_BUNDLE_NAME])
try:
resp = self.__cv_client.api.remove_image_from_element(
assigned_image_facts,
device_facts,
device.hostname,
'netelement'
)

except CvpApiError as catch_error:
MODULE_LOGGER.error('Error removing bundle from device: %s', str(catch_error))
self.__ansible.fail_json(msg='Error removing bundle from device' + device.fqdn + ': ' + catch_error)
else:
if resp['data']['status'] == 'success':
result_data.changed = True
result_data.success = True
result_data.taskIds = resp['data'][Api.task.TASK_IDS]

results.append(result_data)

return results

def apply_configlets(self, user_inventory: DeviceInventory):
"""
apply_configlets Entry point to a list of configlets to device
Expand Down
Loading

0 comments on commit f07ebf7

Please sign in to comment.