4
4
import uuid
5
5
from dataclasses import dataclass
6
6
7
- import pynautobot
8
-
9
7
from understack_workflows .helpers import credential
10
8
from understack_workflows .helpers import parser_nautobot_args
11
9
from understack_workflows .helpers import setup_logger
12
10
from understack_workflows .nautobot import Nautobot
11
+ from understack_workflows .netapp_manager import NetappIPInterfaceConfig
12
+ from understack_workflows .netapp_manager import NetAppManager
13
13
14
14
logger = setup_logger (__name__ , level = logging .INFO )
15
15
16
- # GraphQL query to retrieve virtual machine network information as specified in requirements
17
- VIRTUAL_MACHINES_QUERY = "query ($device_names: [String]){virtual_machines(name: $device_names) {interfaces { name ip_addresses{ address } tagged_vlans { vid }}}}"
16
+ # GraphQL query to retrieve virtual machine network information as specified in
17
+ # requirements
18
+ VIRTUAL_MACHINES_QUERY = (
19
+ "query ($device_names: [String]){virtual_machines(name: $device_names) "
20
+ "{interfaces { name ip_addresses{ address } tagged_vlans { vid }}}}"
21
+ )
18
22
19
23
20
24
@dataclass
@@ -28,32 +32,39 @@ def from_graphql_interface(cls, interface_data):
28
32
"""Create InterfaceInfo from GraphQL interface data with validation.
29
33
30
34
Args:
31
- interface_data: GraphQL interface data containing name, ip_addresses, and tagged_vlans
35
+ interface_data: GraphQL interface data containing name,
36
+ ip_addresses, and tagged_vlans
32
37
33
38
Returns:
34
39
InterfaceInfo: Validated interface information
35
40
36
41
Raises:
37
42
ValueError: If interface has zero or multiple IP addresses or VLANs
38
43
"""
39
- name = interface_data .get (' name' , '' )
40
- ip_addresses = interface_data .get (' ip_addresses' , [])
41
- tagged_vlans = interface_data .get (' tagged_vlans' , [])
44
+ name = interface_data .get (" name" , "" )
45
+ ip_addresses = interface_data .get (" ip_addresses" , [])
46
+ tagged_vlans = interface_data .get (" tagged_vlans" , [])
42
47
43
48
# Validate exactly one IP address
44
49
if len (ip_addresses ) == 0 :
45
50
raise ValueError (f"Interface '{ name } ' has no IP addresses" )
46
51
elif len (ip_addresses ) > 1 :
47
- raise ValueError (f"Interface '{ name } ' has multiple IP addresses: { [ip ['address' ] for ip in ip_addresses ]} " )
52
+ raise ValueError (
53
+ f"Interface '{ name } ' has multiple IP addresses:"
54
+ f" { [ip ['address' ] for ip in ip_addresses ]} "
55
+ )
48
56
49
57
# Validate exactly one tagged VLAN
50
58
if len (tagged_vlans ) == 0 :
51
59
raise ValueError (f"Interface '{ name } ' has no tagged VLANs" )
52
60
elif len (tagged_vlans ) > 1 :
53
- raise ValueError (f"Interface '{ name } ' has multiple tagged VLANs: { [vlan ['vid' ] for vlan in tagged_vlans ]} " )
61
+ raise ValueError (
62
+ f"Interface '{ name } ' has multiple tagged VLANs:"
63
+ f" { [vlan ['vid' ] for vlan in tagged_vlans ]} "
64
+ )
54
65
55
- address = ip_addresses [0 ][' address' ]
56
- vlan = tagged_vlans [0 ][' vid' ]
66
+ address = ip_addresses [0 ][" address" ]
67
+ vlan = tagged_vlans [0 ][" vid" ]
57
68
58
69
return cls (name = name , address = address , vlan = vlan )
59
70
@@ -76,7 +87,7 @@ def from_graphql_vm(cls, vm_data):
76
87
ValueError: If any interface validation fails
77
88
"""
78
89
interfaces = []
79
- for interface_data in vm_data .get (' interfaces' , []):
90
+ for interface_data in vm_data .get (" interfaces" , []):
80
91
interface_info = InterfaceInfo .from_graphql_interface (interface_data )
81
92
interfaces .append (interface_info )
82
93
@@ -107,15 +118,24 @@ def validate_and_normalize_uuid(value: str) -> str:
107
118
def argument_parser ():
108
119
"""Parse command line arguments for netapp network configuration."""
109
120
parser = argparse .ArgumentParser (
110
- description = "Query Nautobot for virtual machine network configuration based on project ID" ,
121
+ description = "Query Nautobot for SVM network configuration and create "
122
+ "NetApp interfaces based on project ID" ,
111
123
)
112
124
113
125
# Add required project_id argument with UUID validation
114
126
parser .add_argument (
115
127
"--project-id" ,
116
128
type = validate_and_normalize_uuid ,
117
129
required = True ,
118
- help = "OpenStack project ID (UUID) to query for virtual machine network configuration"
130
+ help = "OpenStack project ID (UUID) to query for SVM configuration" ,
131
+ )
132
+
133
+ parser .add_argument (
134
+ "--netapp-config-path" ,
135
+ type = str ,
136
+ default = "/etc/netapp/netapp_nvme.conf" ,
137
+ help = "Path to NetApp config with credentials "
138
+ "(default: /etc/netapp/netapp_nvme.conf)" ,
119
139
)
120
140
121
141
# Add Nautobot connection arguments using the helper
@@ -151,43 +171,54 @@ def execute_graphql_query(nautobot_client: Nautobot, project_id: str) -> dict:
151
171
device_name = construct_device_name (project_id )
152
172
variables = {"device_names" : [device_name ]}
153
173
154
- logger .debug (f "Executing GraphQL query for device: { device_name } " )
155
- logger .debug (f "Query variables: { variables } " )
174
+ logger .debug ("Executing GraphQL query for device: %s" , device_name )
175
+ logger .debug ("Query variables: %s" , variables )
156
176
157
177
# Execute the GraphQL query
158
178
try :
159
- result = nautobot_client .session .graphql .query (query = VIRTUAL_MACHINES_QUERY , variables = variables )
179
+ result = nautobot_client .session .graphql .query (
180
+ query = VIRTUAL_MACHINES_QUERY , variables = variables
181
+ )
160
182
except Exception as e :
161
- logger .error (f "Failed to execute GraphQL query: { e } " )
183
+ logger .error ("Failed to execute GraphQL query: %s" , e )
162
184
raise Exception (f"GraphQL query execution failed: { e } " ) from e
163
185
164
186
# Check for GraphQL errors in response
165
187
if not result .json :
166
188
raise Exception ("GraphQL query returned no data" )
167
189
168
190
if result .json .get ("errors" ):
169
- error_messages = [error .get ("message" , str (error )) for error in result .json ["errors" ]]
191
+ error_messages = [
192
+ error .get ("message" , str (error )) for error in result .json ["errors" ]
193
+ ]
170
194
error_details = "; " .join (error_messages )
171
- logger .error (f "GraphQL query returned errors: { error_details } " )
195
+ logger .error ("GraphQL query returned errors: %s" , error_details )
172
196
raise Exception (f"GraphQL query failed with errors: { error_details } " )
173
197
174
198
# Log successful query execution
175
199
data = result .json .get ("data" , {})
176
200
vm_count = len (data .get ("virtual_machines" , []))
177
- logger .info (f"GraphQL query successful. Found { vm_count } virtual machine(s) for device: { device_name } " )
201
+ logger .info (
202
+ "GraphQL query successful. Found %s virtual machine(s) for device: %s" ,
203
+ vm_count ,
204
+ device_name ,
205
+ )
178
206
179
207
return result .json
180
208
181
209
182
- def validate_and_transform_response (graphql_response : dict ) -> list [VirtualMachineNetworkInfo ]:
210
+ def validate_and_transform_response (
211
+ graphql_response : dict ,
212
+ ) -> list [VirtualMachineNetworkInfo ]:
183
213
"""Validate and transform GraphQL response into structured data objects.
184
214
185
215
Args:
186
216
graphql_response: Complete GraphQL response containing data and
187
217
potential errors
188
218
189
219
Returns:
190
- list[VirtualMachineNetworkInfo]: List of validated virtual machine network information
220
+ list[VirtualMachineNetworkInfo]: List of validated SVM network
221
+ information
191
222
192
223
Raises:
193
224
ValueError: If any interface validation fails
@@ -205,17 +236,22 @@ def validate_and_transform_response(graphql_response: dict) -> list[VirtualMachi
205
236
try :
206
237
vm_network_info = VirtualMachineNetworkInfo .from_graphql_vm (vm_data )
207
238
vm_network_infos .append (vm_network_info )
208
- logger .debug (f"Successfully validated VM with { len (vm_network_info .interfaces )} interfaces" )
239
+ logger .debug (
240
+ "Successfully validated VM with %s interfaces" ,
241
+ len (vm_network_info .interfaces ),
242
+ )
209
243
except ValueError as e :
210
- logger .error (f "Interface validation failed: { e } " )
244
+ logger .error ("Interface validation failed: %s" , e )
211
245
raise ValueError (f"Data validation error: { e } " ) from e
212
246
213
- logger .info (f "Successfully validated { len ( vm_network_infos ) } virtual machine(s)" )
247
+ logger .info ("Successfully validated %s virtual machine(s)" , len ( vm_network_infos ) )
214
248
return vm_network_infos
215
249
216
250
217
- def do_action (nautobot_client : Nautobot , project_id : str ) -> tuple [dict , list [VirtualMachineNetworkInfo ]]:
218
- """Execute the main GraphQL query and process results.
251
+ def do_action (
252
+ nautobot_client : Nautobot , netapp_manager : NetAppManager , project_id : str
253
+ ) -> tuple [dict , list [VirtualMachineNetworkInfo ]]:
254
+ """Execute main GraphQL query, process results, and create NetApp interfaces.
219
255
220
256
This function orchestrates the workflow by:
221
257
1. Executing GraphQL query using constructed device name
@@ -226,6 +262,7 @@ def do_action(nautobot_client: Nautobot, project_id: str) -> tuple[dict, list[Vi
226
262
227
263
Args:
228
264
nautobot_client: Nautobot API client instance
265
+ netapp_manager: NetAppManager API client for creating LIF interfaces
229
266
project_id: OpenStack project ID to query for
230
267
231
268
Returns:
@@ -242,7 +279,10 @@ def do_action(nautobot_client: Nautobot, project_id: str) -> tuple[dict, list[Vi
242
279
"""
243
280
try :
244
281
# Execute GraphQL query using constructed device name
245
- logger .info (f"Querying Nautobot for virtual machine network configuration (project_id: { project_id } )" )
282
+ logger .info (
283
+ "Querying Nautobot for SVM network configuration (project_id: %s)" ,
284
+ project_id ,
285
+ )
246
286
raw_response = execute_graphql_query (nautobot_client , project_id )
247
287
248
288
# Process and validate query results
@@ -253,33 +293,75 @@ def do_action(nautobot_client: Nautobot, project_id: str) -> tuple[dict, list[Vi
253
293
device_name = construct_device_name (project_id )
254
294
if validated_data :
255
295
total_interfaces = sum (len (vm .interfaces ) for vm in validated_data )
256
- logger .info (f"Successfully processed { len (validated_data )} virtual machine(s) with { total_interfaces } total interfaces for device: { device_name } " )
296
+ logger .info (
297
+ "Successfully processed %d virtual machine(s) with %d total "
298
+ "interfaces for device: %s" ,
299
+ len (validated_data ),
300
+ total_interfaces ,
301
+ device_name ,
302
+ )
257
303
else :
258
- logger .warning (f"No virtual machines found for device: { device_name } " )
259
-
260
- # Return structured data objects
261
- return raw_response , validated_data
304
+ logger .warning ("No virtual machines found for device: %s" , device_name )
262
305
263
306
except ValueError as e :
264
307
# Handle data validation error scenarios with exit code 3
265
- logger .error (f "Data validation failed: { e } " )
308
+ logger .error ("Data validation failed: %s" , e )
266
309
raise SystemExit (3 ) from e
267
310
268
311
except Exception as e :
269
312
error_msg = str (e )
270
313
271
314
# Handle GraphQL-specific error scenarios with exit code 2
272
315
if "graphql" in error_msg .lower () or "query" in error_msg .lower ():
273
- logger .error (f "GraphQL query failed: { error_msg } " )
316
+ logger .error ("GraphQL query failed: %s" , error_msg )
274
317
raise SystemExit (2 ) from e
275
318
276
319
# Handle other unexpected errors with exit code 2 (query-related)
277
320
else :
278
- logger .error (f "Nautobot error: { error_msg } " )
321
+ logger .error ("Nautobot error: %s" , error_msg )
279
322
raise SystemExit (2 ) from e
280
323
324
+ if validated_data :
325
+ netapp_create_interfaces (netapp_manager , validated_data [0 ], project_id )
326
+
327
+ # Return structured data objects
328
+ return raw_response , validated_data
329
+
330
+
331
+ def netapp_create_interfaces (
332
+ mgr : NetAppManager ,
333
+ nautobot_response : VirtualMachineNetworkInfo ,
334
+ project_id : str ,
335
+ ) -> None :
336
+ """Create NetApp LIF interfaces based on Nautobot VM network configuration.
337
+
338
+ This function converts the validated Nautobot response into NetApp interface
339
+ configurations and creates the corresponding LIF (Logical Interface) on the
340
+ NetApp storage system.
281
341
282
- def format_and_display_output (raw_response : dict , structured_data : list [VirtualMachineNetworkInfo ]) -> None :
342
+ Args:
343
+ mgr: NetAppManager instance for creating LIF interfaces
344
+ nautobot_response: Validated virtual machine network information from
345
+ Nautobot
346
+ project_id: OpenStack project ID for logging and context
347
+
348
+ Returns:
349
+ None
350
+
351
+ Raises:
352
+ Exception: If SVM for the project is not found
353
+ NetAppRestError: If LIF creation fails on the NetApp system
354
+ """
355
+ configs = NetappIPInterfaceConfig .from_nautobot_response (nautobot_response )
356
+ for interface_config in configs :
357
+ logger .info ("Creating LIF %s for project %s" , interface_config .name , project_id )
358
+ mgr .create_lif (project_id , interface_config )
359
+ return
360
+
361
+
362
+ def format_and_display_output (
363
+ raw_response : dict , structured_data : list [VirtualMachineNetworkInfo ]
364
+ ) -> None :
283
365
"""Format and display query results with appropriate logging.
284
366
285
367
This function handles:
@@ -304,14 +386,25 @@ def format_and_display_output(raw_response: dict, structured_data: list[VirtualM
304
386
total_vms = len (structured_data )
305
387
total_interfaces = sum (len (vm .interfaces ) for vm in structured_data )
306
388
307
- logger .info (f"Successfully retrieved network configuration for { total_vms } virtual machine(s)" )
308
- logger .info (f"Total interfaces found: { total_interfaces } " )
389
+ logger .info (
390
+ "Successfully retrieved network configuration for %s virtual machine(s)" ,
391
+ total_vms ,
392
+ )
393
+ logger .info ("Total interfaces found: %s" , total_interfaces )
309
394
310
395
# Log detailed interface information at debug level
311
396
for i , vm in enumerate (structured_data ):
312
- logger .debug (f"Virtual machine { i + 1 } has { len (vm .interfaces )} interface(s):" )
397
+ logger .debug (
398
+ "SVM/Virtual machine %d has {len(vm.interfaces)} interface(s):" ,
399
+ i + 1 ,
400
+ )
313
401
for interface in vm .interfaces :
314
- logger .debug (f" - Interface '{ interface .name } ': { interface .address } (VLAN { interface .vlan } )" )
402
+ logger .debug (
403
+ " - Interface '%s': %s (VLAN %s)" ,
404
+ interface .name ,
405
+ interface .address ,
406
+ interface .vlan ,
407
+ )
315
408
316
409
317
410
def main ():
@@ -341,11 +434,14 @@ def main():
341
434
nb_token = args .nautobot_token or credential ("nb-token" , "token" )
342
435
343
436
# Establish Nautobot connection using parsed arguments
344
- logger .info (f "Connecting to Nautobot at: { args .nautobot_url } " )
437
+ logger .info ("Connecting to Nautobot at: %s" , args .nautobot_url )
345
438
nautobot_client = Nautobot (args .nautobot_url , nb_token , logger = logger )
439
+ netapp_manager = NetAppManager (args .netapp_config_path )
346
440
347
441
# Call do_action() with appropriate parameters
348
- raw_response , structured_data = do_action (nautobot_client , args .project_id )
442
+ raw_response , structured_data = do_action (
443
+ nautobot_client , netapp_manager , args .project_id
444
+ )
349
445
350
446
# Format and display output
351
447
format_and_display_output (raw_response , structured_data )
@@ -360,7 +456,7 @@ def main():
360
456
361
457
except Exception as e :
362
458
# Handle connection errors and other unexpected errors with exit code 1
363
- logger .error (f "Connection or initialization error: { e } " )
459
+ logger .error ("Connection or initialization error: %s" , e )
364
460
return 1
365
461
366
462
0 commit comments