Skip to content

Commit 90fe7f9

Browse files
committed
actually use the Nautobot response now
1 parent 5c7e91e commit 90fe7f9

File tree

2 files changed

+147
-48
lines changed

2 files changed

+147
-48
lines changed

python/understack-workflows/understack_workflows/main/netapp_configure_net.py

Lines changed: 142 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
import uuid
55
from dataclasses import dataclass
66

7-
import pynautobot
8-
97
from understack_workflows.helpers import credential
108
from understack_workflows.helpers import parser_nautobot_args
119
from understack_workflows.helpers import setup_logger
1210
from understack_workflows.nautobot import Nautobot
11+
from understack_workflows.netapp_manager import NetappIPInterfaceConfig
12+
from understack_workflows.netapp_manager import NetAppManager
1313

1414
logger = setup_logger(__name__, level=logging.INFO)
1515

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+
)
1822

1923

2024
@dataclass
@@ -28,32 +32,39 @@ def from_graphql_interface(cls, interface_data):
2832
"""Create InterfaceInfo from GraphQL interface data with validation.
2933
3034
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
3237
3338
Returns:
3439
InterfaceInfo: Validated interface information
3540
3641
Raises:
3742
ValueError: If interface has zero or multiple IP addresses or VLANs
3843
"""
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", [])
4247

4348
# Validate exactly one IP address
4449
if len(ip_addresses) == 0:
4550
raise ValueError(f"Interface '{name}' has no IP addresses")
4651
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+
)
4856

4957
# Validate exactly one tagged VLAN
5058
if len(tagged_vlans) == 0:
5159
raise ValueError(f"Interface '{name}' has no tagged VLANs")
5260
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+
)
5465

55-
address = ip_addresses[0]['address']
56-
vlan = tagged_vlans[0]['vid']
66+
address = ip_addresses[0]["address"]
67+
vlan = tagged_vlans[0]["vid"]
5768

5869
return cls(name=name, address=address, vlan=vlan)
5970

@@ -76,7 +87,7 @@ def from_graphql_vm(cls, vm_data):
7687
ValueError: If any interface validation fails
7788
"""
7889
interfaces = []
79-
for interface_data in vm_data.get('interfaces', []):
90+
for interface_data in vm_data.get("interfaces", []):
8091
interface_info = InterfaceInfo.from_graphql_interface(interface_data)
8192
interfaces.append(interface_info)
8293

@@ -107,15 +118,24 @@ def validate_and_normalize_uuid(value: str) -> str:
107118
def argument_parser():
108119
"""Parse command line arguments for netapp network configuration."""
109120
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",
111123
)
112124

113125
# Add required project_id argument with UUID validation
114126
parser.add_argument(
115127
"--project-id",
116128
type=validate_and_normalize_uuid,
117129
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)",
119139
)
120140

121141
# Add Nautobot connection arguments using the helper
@@ -151,43 +171,54 @@ def execute_graphql_query(nautobot_client: Nautobot, project_id: str) -> dict:
151171
device_name = construct_device_name(project_id)
152172
variables = {"device_names": [device_name]}
153173

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)
156176

157177
# Execute the GraphQL query
158178
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+
)
160182
except Exception as e:
161-
logger.error(f"Failed to execute GraphQL query: {e}")
183+
logger.error("Failed to execute GraphQL query: %s", e)
162184
raise Exception(f"GraphQL query execution failed: {e}") from e
163185

164186
# Check for GraphQL errors in response
165187
if not result.json:
166188
raise Exception("GraphQL query returned no data")
167189

168190
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+
]
170194
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)
172196
raise Exception(f"GraphQL query failed with errors: {error_details}")
173197

174198
# Log successful query execution
175199
data = result.json.get("data", {})
176200
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+
)
178206

179207
return result.json
180208

181209

182-
def validate_and_transform_response(graphql_response: dict) -> list[VirtualMachineNetworkInfo]:
210+
def validate_and_transform_response(
211+
graphql_response: dict,
212+
) -> list[VirtualMachineNetworkInfo]:
183213
"""Validate and transform GraphQL response into structured data objects.
184214
185215
Args:
186216
graphql_response: Complete GraphQL response containing data and
187217
potential errors
188218
189219
Returns:
190-
list[VirtualMachineNetworkInfo]: List of validated virtual machine network information
220+
list[VirtualMachineNetworkInfo]: List of validated SVM network
221+
information
191222
192223
Raises:
193224
ValueError: If any interface validation fails
@@ -205,17 +236,22 @@ def validate_and_transform_response(graphql_response: dict) -> list[VirtualMachi
205236
try:
206237
vm_network_info = VirtualMachineNetworkInfo.from_graphql_vm(vm_data)
207238
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+
)
209243
except ValueError as e:
210-
logger.error(f"Interface validation failed: {e}")
244+
logger.error("Interface validation failed: %s", e)
211245
raise ValueError(f"Data validation error: {e}") from e
212246

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))
214248
return vm_network_infos
215249

216250

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.
219255
220256
This function orchestrates the workflow by:
221257
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
226262
227263
Args:
228264
nautobot_client: Nautobot API client instance
265+
netapp_manager: NetAppManager API client for creating LIF interfaces
229266
project_id: OpenStack project ID to query for
230267
231268
Returns:
@@ -242,7 +279,10 @@ def do_action(nautobot_client: Nautobot, project_id: str) -> tuple[dict, list[Vi
242279
"""
243280
try:
244281
# 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+
)
246286
raw_response = execute_graphql_query(nautobot_client, project_id)
247287

248288
# Process and validate query results
@@ -253,33 +293,75 @@ def do_action(nautobot_client: Nautobot, project_id: str) -> tuple[dict, list[Vi
253293
device_name = construct_device_name(project_id)
254294
if validated_data:
255295
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+
)
257303
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)
262305

263306
except ValueError as e:
264307
# 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)
266309
raise SystemExit(3) from e
267310

268311
except Exception as e:
269312
error_msg = str(e)
270313

271314
# Handle GraphQL-specific error scenarios with exit code 2
272315
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)
274317
raise SystemExit(2) from e
275318

276319
# Handle other unexpected errors with exit code 2 (query-related)
277320
else:
278-
logger.error(f"Nautobot error: {error_msg}")
321+
logger.error("Nautobot error: %s", error_msg)
279322
raise SystemExit(2) from e
280323

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.
281341
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:
283365
"""Format and display query results with appropriate logging.
284366
285367
This function handles:
@@ -304,14 +386,25 @@ def format_and_display_output(raw_response: dict, structured_data: list[VirtualM
304386
total_vms = len(structured_data)
305387
total_interfaces = sum(len(vm.interfaces) for vm in structured_data)
306388

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)
309394

310395
# Log detailed interface information at debug level
311396
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+
)
313401
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+
)
315408

316409

317410
def main():
@@ -341,11 +434,14 @@ def main():
341434
nb_token = args.nautobot_token or credential("nb-token", "token")
342435

343436
# 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)
345438
nautobot_client = Nautobot(args.nautobot_url, nb_token, logger=logger)
439+
netapp_manager = NetAppManager(args.netapp_config_path)
346440

347441
# 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+
)
349445

350446
# Format and display output
351447
format_and_display_output(raw_response, structured_data)
@@ -360,7 +456,7 @@ def main():
360456

361457
except Exception as e:
362458
# 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)
364460
return 1
365461

366462

python/understack-workflows/understack_workflows/netapp_manager.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
from dataclasses import dataclass
88
from functools import cached_property
9+
from typing import TYPE_CHECKING
910

1011
import urllib3
1112
from netapp_ontap import config
@@ -20,7 +21,9 @@
2021
from netapp_ontap.resources import Volume
2122

2223
from understack_workflows.helpers import setup_logger
23-
from understack_workflows.main.netapp_configure_net import VirtualMachineNetworkInfo
24+
25+
if TYPE_CHECKING:
26+
from understack_workflows.main.netapp_configure_net import VirtualMachineNetworkInfo
2427

2528
logger = setup_logger(__name__)
2629

@@ -64,7 +67,7 @@ def desired_node_number(self) -> int:
6467
raise ValueError("Cannot determine node index from name %s", self.name)
6568

6669
@classmethod
67-
def from_nautobot_response(cls, response: VirtualMachineNetworkInfo):
70+
def from_nautobot_response(cls, response: "VirtualMachineNetworkInfo"):
6871
result = []
6972
for interface in response.interfaces:
7073
address, _ = interface.address.split("/")

0 commit comments

Comments
 (0)