2
2
# -*- coding: utf-8 -*-
3
3
4
4
# Copyright: (c) 2023, Shreyas Srish (@shrsr) <[email protected] >
5
+ # Copyright: (c) 2025, Sabari Jaganathan (@sajagana) <[email protected] >
5
6
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7
7
8
from __future__ import absolute_import , division , print_function
19
20
- Manages importing the cluster configuration using a backup.
20
21
author:
21
22
- Shreyas Srish (@shrsr)
23
+ - Sabari Jaganathan (@sajagana)
22
24
options:
23
25
name:
24
26
description:
41
43
- This key is required when querying or deleting a restored job among multiple restored jobs that have the same name.
42
44
- This key can be obtained by querying a restored job.
43
45
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
44
64
state:
45
65
description:
46
66
- Use C(restore) for importing a backup of the cluster config.
84
104
85
105
from ansible .module_utils .basic import AnsibleModule
86
106
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
87
108
88
109
89
110
def main ():
@@ -94,14 +115,16 @@ def main():
94
115
file_location = dict (type = "str" ),
95
116
restore_key = dict (type = "str" , no_log = False ),
96
117
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" ),
97
121
)
98
122
99
123
module = AnsibleModule (
100
124
argument_spec = argument_spec ,
101
125
supports_check_mode = True ,
102
126
required_if = [
103
- ["state" , "restore" , ["name" , "encryption_key" , "file_location" ]],
104
- ["state" , "absent" , ["name" ]],
127
+ ["state" , "restore" , ["encryption_key" ]],
105
128
],
106
129
)
107
130
@@ -112,10 +135,89 @@ def main():
112
135
restore_key = nd .params .get ("restore_key" )
113
136
file_location = nd .params .get ("file_location" )
114
137
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" )
115
141
116
142
if encryption_key is not None and len (encryption_key ) < 8 :
117
143
nd .fail_json ("The encryption key must have a minium of 8 characters." )
118
144
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
+
119
221
path = "/nexus/infra/api/platform/v1/imports"
120
222
# The below path for GET operation is to be replaced by an official documented API endpoint once it becomes available.
121
223
restored_objs = nd .query_obj ("/api/config/class/imports" )
@@ -156,8 +258,6 @@ def main():
156
258
nd .request (path , method = "POST" , data = payload , file = file_location , file_key = "importfile" , output_format = "raw" )
157
259
nd .existing = nd .proposed
158
260
159
- nd .exit_json ()
160
-
161
261
162
262
if __name__ == "__main__" :
163
263
main ()
0 commit comments