Skip to content

Commit af68b91

Browse files
committed
[ignore] fixed nd_backup_restore module
1 parent a8fb768 commit af68b91

File tree

4 files changed

+792
-243
lines changed

4 files changed

+792
-243
lines changed

plugins/modules/nd_backup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# -*- coding: utf-8 -*-
33

44
# Copyright: (c) 2023, Shreyas Srish (@shrsr) <[email protected]>
5+
# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) <[email protected]>
56
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
67

78
from __future__ import absolute_import, division, print_function
@@ -19,6 +20,7 @@
1920
- Manages backup of the cluster configuration.
2021
author:
2122
- Shreyas Srish (@shrsr)
23+
- Sabari Jaganathan (@sajagana)
2224
options:
2325
name:
2426
description:

plugins/modules/nd_backup_restore.py

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# -*- coding: utf-8 -*-
33

44
# Copyright: (c) 2023, Shreyas Srish (@shrsr) <[email protected]>
5+
# Copyright: (c) 2025, Sabari Jaganathan (@sajagana) <[email protected]>
56
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
67

78
from __future__ import absolute_import, division, print_function
@@ -19,6 +20,7 @@
1920
- Manages importing the cluster configuration using a backup.
2021
author:
2122
- Shreyas Srish (@shrsr)
23+
- Sabari Jaganathan (@sajagana)
2224
options:
2325
name:
2426
description:
@@ -41,6 +43,24 @@
4143
- This key is required when querying or deleting a restored job among multiple restored jobs that have the same name.
4244
- This key can be obtained by querying a restored job.
4345
type: str
46+
ignore_persistent_ips:
47+
description:
48+
- When the O(ignore_persistent_ips=true), will overwrite the existing external service IP addresses configured on the Nexus Dashboard.
49+
type: bool
50+
aliases: [ ignore_external_service_ip_configuration ]
51+
restore_type:
52+
description:
53+
- This parameter is only supported on ND v3.2.1 and later.
54+
- The O(restore_type=config_only) option restores only configuration settings of the Nexus Dashboard.
55+
- The O(backup_type=full) option restores the entire settings of the Nexus Dashboard.
56+
type: str
57+
choices: [ config_only, full ]
58+
default: config_only
59+
aliases: [ type ]
60+
remote_location:
61+
description:
62+
- The name of the remote storage location. This parameter is only supported on ND v3.2.1 and later.
63+
type: str
4464
state:
4565
description:
4666
- Use C(restore) for importing a backup of the cluster config.
@@ -84,6 +104,7 @@
84104

85105
from ansible.module_utils.basic import AnsibleModule
86106
from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec
107+
from ansible_collections.cisco.nd.plugins.module_utils.utils import snake_to_camel
87108

88109

89110
def main():
@@ -94,14 +115,16 @@ def main():
94115
file_location=dict(type="str"),
95116
restore_key=dict(type="str", no_log=False),
96117
state=dict(type="str", default="restore", choices=["restore", "query", "absent"]),
118+
ignore_persistent_ips=dict(type="bool", aliases=["ignore_external_service_ip_configuration"]),
119+
restore_type=dict(type="str", default="config_only", choices=["config_only", "full"], aliases=["type"]),
120+
remote_location=dict(type="str"),
97121
)
98122

99123
module = AnsibleModule(
100124
argument_spec=argument_spec,
101125
supports_check_mode=True,
102126
required_if=[
103-
["state", "restore", ["name", "encryption_key", "file_location"]],
104-
["state", "absent", ["name"]],
127+
["state", "restore", ["encryption_key"]],
105128
],
106129
)
107130

@@ -112,10 +135,89 @@ def main():
112135
restore_key = nd.params.get("restore_key")
113136
file_location = nd.params.get("file_location")
114137
state = nd.params.get("state")
138+
ignore_persistent_ips = nd.params.get("ignore_persistent_ips")
139+
restore_type = snake_to_camel(nd.params.get("restore_type"))
140+
remote_location = nd.params.get("remote_location")
115141

116142
if encryption_key is not None and len(encryption_key) < 8:
117143
nd.fail_json("The encryption key must have a minium of 8 characters.")
118144

145+
if nd.version < "3.2.1":
146+
nd_backup_restore_before_3_2_1(nd, name, encryption_key, file_location, restore_key, state, module)
147+
elif nd.version >= "3.2.1":
148+
nd_backup_restore_from_3_2_1(nd, name, encryption_key, file_location, state, module, ignore_persistent_ips, restore_type, remote_location)
149+
nd.exit_json()
150+
151+
152+
def nd_backup_restore_from_3_2_1(nd, name, encryption_key, file_location, state, module, ignore_persistent_ips, restore_type, remote_location):
153+
if name and (remote_location or file_location):
154+
nd.fail_json("The parameters name and (remote_location or file_location) cannot be specified at the same time.")
155+
156+
nd.existing = nd.query_obj("/api/v1/infra/backups/status")
157+
import_path = "/api/v1/infra/backups/actions/import"
158+
159+
# Remove backup status (not idempotent)
160+
if state == "absent" and (not nd.existing or nd.existing.get("state") != "processing"):
161+
nd.previous = nd.existing
162+
if not module.check_mode:
163+
nd.request(import_path, method="DELETE")
164+
165+
# Restore from backup (not idempotent)
166+
elif state == "restore" and (not nd.existing or nd.existing.get("state") != "processing"):
167+
168+
if not module.check_mode: # Need to delete the imported file before starting the restore process
169+
nd.request(import_path, method="DELETE")
170+
171+
import_payload = {"encryptionKey": encryption_key}
172+
173+
if remote_location and file_location and not name:
174+
import_payload.update({"source": remote_location, "path": file_location})
175+
elif not remote_location and file_location and not name:
176+
# Local file upload
177+
if not module.check_mode:
178+
import_payload["path"] = nd.request(
179+
"/api/action/class/backuprestore/file-upload", method="POST", data=None, file=file_location, file_key="files", output_format="raw"
180+
)
181+
elif name:
182+
import_payload["name"] = name.split(".")[0] # Restore operation requires only name of the backup file
183+
184+
nd.sanitize(import_payload, collate=True)
185+
186+
restore_payload = {
187+
"ignorePersistentIPs": ignore_persistent_ips
188+
or False, # add note to the document saying that ignore_persistent_ips set to false when it is not specified
189+
"type": restore_type or "configOnly", # add note to the document saying that restore_type set to configOnly when it is not specified
190+
}
191+
nd_payload = {
192+
"fileUploadPayload": {"fileLocation": file_location},
193+
"importPayload": import_payload,
194+
"restorePayload": restore_payload,
195+
}
196+
nd.sanitize(nd_payload, collate=True)
197+
198+
if not module.check_mode:
199+
nd.request(import_path, method="POST", data=import_payload)
200+
nd.request("/api/v1/infra/backups/actions/restore", method="POST", data=restore_payload)
201+
nd.existing = nd.query_obj("/api/v1/infra/backups/status")
202+
else:
203+
nd.existing = nd.proposed
204+
205+
# Operation not allowed if backup status is processing
206+
elif state != "query" and nd.existing and nd.existing.get("state") == "processing":
207+
nd.fail_json(
208+
msg="The {0} operation could not proceed because a system {1} is in progress ({2}% complete).".format(
209+
state, nd.existing.get("operation"), nd.existing.get("details", {}).get("progress")
210+
)
211+
)
212+
213+
214+
def nd_backup_restore_before_3_2_1(nd, name, encryption_key, file_location, restore_key, state, module):
215+
if state == "restore" and not (name and encryption_key and file_location):
216+
nd.fail_json("state is restore but all/one of the following are missing: name, encryption_key, file_location")
217+
218+
if state == "absent" and not name:
219+
nd.fail_json("state is absent but all of the following are missing: name")
220+
119221
path = "/nexus/infra/api/platform/v1/imports"
120222
# The below path for GET operation is to be replaced by an official documented API endpoint once it becomes available.
121223
restored_objs = nd.query_obj("/api/config/class/imports")
@@ -156,8 +258,6 @@ def main():
156258
nd.request(path, method="POST", data=payload, file=file_location, file_key="importfile", output_format="raw")
157259
nd.existing = nd.proposed
158260

159-
nd.exit_json()
160-
161261

162262
if __name__ == "__main__":
163263
main()

plugins/modules/nd_rest.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
- The file path containing the body of the HTTP request.
4949
type: path
5050
aliases: [ config_file ]
51+
ignore_previous_state:
52+
description:
53+
- This parameter ignores the object's previous state.
54+
type: bool
5155
extends_documentation_fragment:
5256
- cisco.nd.modules
5357
- cisco.nd.check_mode
@@ -212,6 +216,7 @@ def main():
212216
),
213217
content=dict(type="raw", aliases=["payload"]),
214218
file_path=dict(type="path", aliases=["config_file"]),
219+
ignore_previous_state=dict(type="bool"),
215220
)
216221

217222
module = AnsibleModule(
@@ -222,6 +227,7 @@ def main():
222227
content = module.params.get("content")
223228
path = module.params.get("path")
224229
file_path = module.params.get("config_file")
230+
ignore_previous_state = module.params.get("ignore_previous_state")
225231

226232
nd = NDModule(module)
227233

@@ -251,7 +257,8 @@ def main():
251257

252258
# Append previous state of the object
253259
if method in ("PUT", "DELETE", "PATCH"):
254-
nd.existing = nd.previous = sanitize(nd.query_obj(path, ignore_not_found_error=True), ND_REST_KEYS_TO_SANITIZE)
260+
if not ignore_previous_state:
261+
nd.existing = nd.previous = sanitize(nd.query_obj(path, ignore_not_found_error=True), ND_REST_KEYS_TO_SANITIZE)
255262
nd.result["previous"] = nd.previous
256263

257264
# Perform request

0 commit comments

Comments
 (0)