-
Notifications
You must be signed in to change notification settings - Fork 19
Added ND 4.1 version support to the nd_backup and nd_backup_restore module (DCNE-452) #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) <[email protected]> | ||
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
|
||
from __future__ import absolute_import, division, print_function | ||
|
||
__metaclass__ = type | ||
|
||
|
||
def snake_to_camel(snake_str, upper_case_components=None): | ||
if snake_str is not None and "_" in snake_str: | ||
if upper_case_components is None: | ||
upper_case_components = [] | ||
components = snake_str.split("_") | ||
camel_case_str = components[0] | ||
|
||
for component in components[1:]: | ||
if component in upper_case_components: | ||
camel_case_str += component.upper() | ||
else: | ||
camel_case_str += component.title() | ||
|
||
return camel_case_str | ||
else: | ||
return snake_str |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
# -*- coding: utf-8 -*- | ||
|
||
# Copyright: (c) 2023, Shreyas Srish (@shrsr) <[email protected]> | ||
# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) <[email protected]> | ||
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
|
||
from __future__ import absolute_import, division, print_function | ||
|
@@ -19,6 +20,7 @@ | |
- Manages backup of the cluster configuration. | ||
author: | ||
- Shreyas Srish (@shrsr) | ||
- Sabari Jaganathan (@sajagana) | ||
options: | ||
name: | ||
description: | ||
|
@@ -28,38 +30,84 @@ | |
encryption_key: | ||
description: | ||
- The encryption_key for a backup file. | ||
- A minimum of 8 alphanumeric characters is required. | ||
type: str | ||
file_location: | ||
description: | ||
- The download path and file name for a backup. | ||
- When O(file_location) is specified, the backup will be created and automatically downloaded to the local machine at the designated path. | ||
type: str | ||
backup_key: | ||
description: | ||
- The key generated by ND during creation of a backup. | ||
- This key is required when querying or deleting a backup among multiple backups that have the same name. | ||
- This key can be obtained by querying the backup. | ||
- This parameter is not supported on ND v3.2.1 and later. | ||
type: str | ||
remote_location: | ||
description: | ||
- The name of the remote storage location. This parameter is only supported on ND v3.2.1 and later. | ||
- If the O(remote_location) parameter is not specified or O(remote_location="") during backup creation, a local backup will be created. | ||
type: str | ||
backup_type: | ||
description: | ||
- This parameter is only supported on ND v3.2.1 and later. | ||
- The O(backup_type=config_only) option creates a snapshot that specifically captures the configuration settings of the Nexus Dashboard. | ||
- The O(backup_type=full) option creates a complete snapshot of the entire Nexus Dashboard. | ||
type: str | ||
choices: [ config_only, full ] | ||
default: config_only | ||
aliases: [ type ] | ||
state: | ||
description: | ||
- Use C(backup) for creating a backup of the cluster config. | ||
- Use C(query) for listing all the backed up files. | ||
- Use C(absent) for deleting a backup job. | ||
- Use O(state=backup) for creating and downloading a backup of the cluster config for the ND versions < 3.2.1. | ||
- Use O(state=backup) to create a cluster configuration backup. Automatic download is not supported for the ND versions >= 3.2.1. | ||
- After creation, use O(state=download) to download the backup file. | ||
- Use O(state=download) downloading a backup to the local machine, the O(state=download) is only supported on ND v3.2.1 and later. | ||
- Use O(state=query) for listing all the backed up files. | ||
- Use O(state=absent) for deleting a backup job. | ||
type: str | ||
choices: [ backup, query, absent ] | ||
choices: [ backup, download, query, absent ] | ||
default: backup | ||
extends_documentation_fragment: | ||
- cisco.nd.modules | ||
- cisco.nd.check_mode | ||
""" | ||
|
||
EXAMPLES = r""" | ||
- name: Create a Backup | ||
- name: Create a backup for ND versions < 3.2.1 | ||
cisco.nd.nd_backup: | ||
name: nexus | ||
encryption_key: testtest | ||
file_location: ./nexus.tgz | ||
state: backup | ||
|
||
- name: Create a remote backup for ND versions >= 3.2.1 | ||
cisco.nd.nd_backup: | ||
name: nexus | ||
encryption_key: testtest1 | ||
remote_location: remote_machine | ||
state: backup | ||
|
||
- name: Create a local backup for ND versions >= 3.2.1 | ||
cisco.nd.nd_backup: | ||
name: nexus | ||
encryption_key: testtest1 | ||
state: backup | ||
|
||
- name: Create a backup and download it to the local machine for ND versions >= 3.2.1 | ||
cisco.nd.nd_backup: | ||
name: nexus | ||
file_location: ./nexus.tgz | ||
encryption_key: testtest1 | ||
state: backup | ||
|
||
- name: Download a local/remote backup for ND versions >= 3.2.1 | ||
cisco.nd.nd_backup: | ||
name: nexus | ||
state: download | ||
file_location: ./nexus.tgz | ||
|
||
- name: Query a Backup job | ||
cisco.nd.nd_backup: | ||
name: nexus | ||
|
@@ -83,6 +131,7 @@ | |
from ansible.module_utils._text import to_bytes | ||
from ansible.module_utils.basic import AnsibleModule | ||
from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec, write_file | ||
from ansible_collections.cisco.nd.plugins.module_utils.utils import snake_to_camel | ||
|
||
|
||
def main(): | ||
|
@@ -92,15 +141,18 @@ def main(): | |
encryption_key=dict(type="str", no_log=False), | ||
file_location=dict(type="str"), | ||
backup_key=dict(type="str", no_log=False), | ||
state=dict(type="str", default="backup", choices=["backup", "query", "absent"]), | ||
remote_location=dict(type="str"), | ||
backup_type=dict(type="str", default="config_only", choices=["config_only", "full"], aliases=["type"]), | ||
state=dict(type="str", default="backup", choices=["backup", "download", "query", "absent"]), | ||
) | ||
|
||
module = AnsibleModule( | ||
argument_spec=argument_spec, | ||
supports_check_mode=True, | ||
required_if=[ | ||
["state", "backup", ["name", "encryption_key", "file_location"]], | ||
["state", "backup", ["name", "encryption_key"]], | ||
["state", "absent", ["name"]], | ||
["state", "download", ["name", "file_location"]], | ||
], | ||
) | ||
|
||
|
@@ -110,8 +162,75 @@ def main(): | |
encryption_key = nd.params.get("encryption_key") | ||
backup_key = nd.params.get("backup_key") | ||
file_location = nd.params.get("file_location") | ||
remote_location = nd.params.get("remote_location") | ||
backup_type = nd.params.get("backup_type") | ||
state = nd.params.get("state") | ||
|
||
if nd.version < "3.2.1": | ||
if not file_location and state in ["backup", "download"]: | ||
nd.fail_json("Parameter 'file_location' is required when state is 'backup|download' for ND versions < 3.2.1.") | ||
nd_backup_before_3_2_1(module, nd, name, encryption_key, file_location, backup_key, state) | ||
elif nd.version >= "3.2.1": | ||
nd_backup_from_3_2_1(module, nd, name, encryption_key, file_location, remote_location, backup_type, state) | ||
|
||
nd.exit_json() | ||
|
||
|
||
def nd_backup_from_3_2_1(module, nd, name, encryption_key, file_location, remote_location, backup_type, state): | ||
if encryption_key is not None: | ||
if len(encryption_key) < 8: | ||
nd.fail_json("Please provide a minimum of 8 alphanumeric characters for the encryption key.") | ||
elif not (any(char.isalpha() for char in encryption_key) and any(char.isdigit() for char in encryption_key) and encryption_key.isalnum()): | ||
nd.fail_json("The encryption_key must contain at least one letter and one number, and have a minimum length of 8 characters.") | ||
Comment on lines
+181
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is ND not failing on these errors? I thought the agreement was made before to do the least amount of validation and let ND API handle it |
||
|
||
path = "/api/v1/infra/backups" | ||
backups = nd.query_obj(path) | ||
if name and backups: | ||
for backup in backups.get("backups", []): | ||
if backup.get("name") == name: | ||
nd.existing = backup | ||
break | ||
else: | ||
nd.existing = backups.get("backups", []) | ||
|
||
if state == "absent" and nd.existing: | ||
nd.previous = nd.existing | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it intended to do this here instead of already in the querying part? |
||
if not module.check_mode: | ||
nd.request("{0}/{1}".format(path, name), method="DELETE") | ||
nd.existing = {} | ||
|
||
elif state == "backup": | ||
if not nd.existing: | ||
payload = { | ||
"name": name, | ||
"type": snake_to_camel(backup_type), | ||
"destination": remote_location if remote_location else "", | ||
"encryptionKey": encryption_key, | ||
} | ||
nd.sanitize(payload, collate=True) | ||
|
||
if not module.check_mode: | ||
# Creates backup file and returns None | ||
nd.request(path, method="POST", data=payload) | ||
|
||
# Fetching the backup object details to set module current value | ||
nd.existing = nd.request("{0}/{1}".format(path, name), method="GET") | ||
|
||
if file_location: | ||
response = nd.request("{0}/{1}/actions/download".format(path, name), method="GET", data=None, output_format="raw") | ||
write_file(module, file_location, to_bytes(response)) | ||
elif module.check_mode: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. else? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also if |
||
nd.existing = nd.proposed | ||
else: | ||
nd.previous = nd.existing | ||
|
||
elif state == "download" and file_location and nd.existing: | ||
if not module.check_mode: | ||
Comment on lines
+227
to
+228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not combine this when you are already making a complicated elif |
||
response = nd.request("{0}/{1}/actions/download".format(path, name), method="GET", data=None, output_format="raw") | ||
write_file(module, file_location, to_bytes(response)) | ||
|
||
|
||
def nd_backup_before_3_2_1(module, nd, name, encryption_key, file_location, backup_key, state): | ||
if encryption_key is not None and len(encryption_key) < 8: | ||
nd.fail_json("Please provide a minimum of 8 characters for the encryption key.") | ||
|
||
|
@@ -158,8 +277,6 @@ def main(): | |
write_file(module, file_location, to_bytes(response)) | ||
nd.existing = nd.proposed | ||
|
||
nd.exit_json() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not else here?