catocli 2.1.3__py3-none-any.whl → 2.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of catocli might be problematic. Click here for more details.
- catocli/Utils/clidriver.py +20 -9
- catocli/Utils/cliutils.py +45 -17
- catocli/Utils/csv_formatter.py +652 -0
- catocli/__init__.py +2 -2
- catocli/clisettings.json +35 -0
- catocli/parsers/custom/export_rules/__init__.py +0 -4
- catocli/parsers/custom/export_sites/__init__.py +17 -5
- catocli/parsers/custom/export_sites/export_sites.py +826 -53
- catocli/parsers/custom/import_sites_to_tf/__init__.py +44 -16
- catocli/parsers/custom/import_sites_to_tf/import_sites_to_tf.py +859 -442
- catocli/parsers/customParserApiClient.py +444 -38
- catocli/parsers/custom_private/__init__.py +18 -0
- catocli/parsers/mutation_accountManagement/__init__.py +21 -0
- catocli/parsers/mutation_accountManagement_disableAccount/README.md +15 -0
- catocli/parsers/mutation_admin/__init__.py +12 -0
- catocli/parsers/mutation_container/__init__.py +18 -0
- catocli/parsers/mutation_enterpriseDirectory/__init__.py +8 -0
- catocli/parsers/mutation_groups/__init__.py +6 -0
- catocli/parsers/mutation_hardware/__init__.py +2 -0
- catocli/parsers/mutation_licensing/__init__.py +24 -0
- catocli/parsers/mutation_licensing_updateCommercialLicense/README.md +19 -0
- catocli/parsers/mutation_policy/__init__.py +861 -483
- catocli/parsers/mutation_policy_antiMalwareFileHash_addRule/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_addSection/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_createPolicyRevision/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_discardPolicyRevision/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_moveRule/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_moveSection/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_publishPolicyRevision/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_removeRule/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_removeSection/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_updatePolicy/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_updateRule/README.md +20 -0
- catocli/parsers/mutation_policy_antiMalwareFileHash_updateSection/README.md +20 -0
- catocli/parsers/mutation_sandbox/__init__.py +4 -0
- catocli/parsers/mutation_site/__init__.py +72 -0
- catocli/parsers/mutation_sites/__init__.py +72 -0
- catocli/parsers/mutation_xdr/__init__.py +6 -0
- catocli/parsers/query_accountBySubdomain/__init__.py +2 -0
- catocli/parsers/query_accountManagement/__init__.py +2 -0
- catocli/parsers/query_accountMetrics/__init__.py +6 -0
- catocli/parsers/query_accountRoles/__init__.py +2 -0
- catocli/parsers/query_accountSnapshot/__init__.py +2 -0
- catocli/parsers/query_admin/__init__.py +2 -0
- catocli/parsers/query_admins/__init__.py +2 -0
- catocli/parsers/query_appStats/__init__.py +6 -0
- catocli/parsers/query_appStatsTimeSeries/README.md +3 -0
- catocli/parsers/query_appStatsTimeSeries/__init__.py +6 -0
- catocli/parsers/query_auditFeed/__init__.py +2 -0
- catocli/parsers/query_catalogs/__init__.py +2 -0
- catocli/parsers/query_container/__init__.py +2 -0
- catocli/parsers/query_devices/README.md +1 -1
- catocli/parsers/query_devices/__init__.py +2 -0
- catocli/parsers/query_enterpriseDirectory/__init__.py +2 -0
- catocli/parsers/query_entityLookup/__init__.py +2 -0
- catocli/parsers/query_events/__init__.py +2 -0
- catocli/parsers/query_eventsFeed/README.md +1 -1
- catocli/parsers/query_eventsFeed/__init__.py +2 -0
- catocli/parsers/query_eventsTimeSeries/__init__.py +2 -0
- catocli/parsers/query_groups/__init__.py +6 -0
- catocli/parsers/query_hardware/README.md +1 -1
- catocli/parsers/query_hardware/__init__.py +2 -0
- catocli/parsers/query_hardwareManagement/__init__.py +2 -0
- catocli/parsers/query_licensing/__init__.py +2 -0
- catocli/parsers/query_policy/__init__.py +85 -48
- catocli/parsers/query_policy_antiMalwareFileHash_policy/README.md +19 -0
- catocli/parsers/query_popLocations/__init__.py +2 -0
- catocli/parsers/query_sandbox/__init__.py +2 -0
- catocli/parsers/query_servicePrincipalAdmin/__init__.py +2 -0
- catocli/parsers/query_site/__init__.py +33 -0
- catocli/parsers/query_siteLocation/__init__.py +2 -0
- catocli/parsers/query_site_siteGeneralDetails/README.md +19 -0
- catocli/parsers/query_socketPortMetrics/__init__.py +2 -0
- catocli/parsers/query_socketPortMetricsTimeSeries/__init__.py +6 -0
- catocli/parsers/query_subDomains/__init__.py +2 -0
- catocli/parsers/query_xdr/__init__.py +4 -0
- catocli/parsers/raw/__init__.py +3 -1
- {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/METADATA +1 -1
- {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/RECORD +107 -72
- models/mutation.accountManagement.disableAccount.json +545 -0
- models/mutation.licensing.updateCommercialLicense.json +931 -0
- models/mutation.policy.antiMalwareFileHash.addRule.json +2068 -0
- models/mutation.policy.antiMalwareFileHash.addSection.json +1350 -0
- models/mutation.policy.antiMalwareFileHash.createPolicyRevision.json +1822 -0
- models/mutation.policy.antiMalwareFileHash.discardPolicyRevision.json +1758 -0
- models/mutation.policy.antiMalwareFileHash.moveRule.json +1552 -0
- models/mutation.policy.antiMalwareFileHash.moveSection.json +1251 -0
- models/mutation.policy.antiMalwareFileHash.publishPolicyRevision.json +1813 -0
- models/mutation.policy.antiMalwareFileHash.removeRule.json +1204 -0
- models/mutation.policy.antiMalwareFileHash.removeSection.json +954 -0
- models/mutation.policy.antiMalwareFileHash.updatePolicy.json +1834 -0
- models/mutation.policy.antiMalwareFileHash.updateRule.json +1757 -0
- models/mutation.policy.antiMalwareFileHash.updateSection.json +1105 -0
- models/mutation.site.updateSiteGeneralDetails.json +3 -3
- models/mutation.sites.updateSiteGeneralDetails.json +3 -3
- models/query.devices.json +448 -62
- models/query.events.json +216 -0
- models/query.eventsFeed.json +48 -0
- models/query.eventsTimeSeries.json +144 -0
- models/query.hardware.json +224 -0
- models/query.policy.antiMalwareFileHash.policy.json +1583 -0
- models/query.site.siteGeneralDetails.json +899 -0
- schema/catolib.py +51 -4
- {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/WHEEL +0 -0
- {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/entry_points.txt +0 -0
- {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/licenses/LICENSE +0 -0
- {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/top_level.txt +0 -0
|
@@ -2,12 +2,57 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
import traceback
|
|
4
4
|
import sys
|
|
5
|
+
import ipaddress
|
|
6
|
+
import csv
|
|
5
7
|
from datetime import datetime
|
|
6
8
|
from graphql_client.api.call_api import ApiClient, CallApi
|
|
7
9
|
from graphql_client.api_client import ApiException
|
|
8
10
|
from ..customLib import writeDataToFile, makeCall, getAccountID
|
|
9
11
|
from ....Utils.cliutils import load_cli_settings
|
|
10
12
|
|
|
13
|
+
def calculateLocalIp(subnet):
|
|
14
|
+
"""
|
|
15
|
+
Calculate the first usable IP address from a subnet/CIDR notation.
|
|
16
|
+
Returns the network address + 1 (first host IP).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
subnet (str): Subnet in CIDR notation (e.g., "192.168.1.0/24")
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
str: First usable IP address, or None if invalid subnet
|
|
23
|
+
"""
|
|
24
|
+
if not subnet or subnet == "":
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Parse the subnet
|
|
29
|
+
network = ipaddress.IPv4Network(subnet, strict=False)
|
|
30
|
+
|
|
31
|
+
# Get the first usable IP (network address + 1)
|
|
32
|
+
# For /31 and /32 networks, return the network address itself
|
|
33
|
+
if network.prefixlen >= 31:
|
|
34
|
+
return str(network.network_address)
|
|
35
|
+
else:
|
|
36
|
+
# Return network + 1 (first host address)
|
|
37
|
+
first_host = network.network_address + 1
|
|
38
|
+
return str(first_host)
|
|
39
|
+
|
|
40
|
+
except (ipaddress.AddressValueError, ipaddress.NetmaskValueError, ValueError) as e:
|
|
41
|
+
# Invalid subnet format
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def export_socket_sites_dispatcher(args, configuration):
|
|
45
|
+
"""
|
|
46
|
+
Dispatcher function that routes to JSON or CSV export based on format argument
|
|
47
|
+
"""
|
|
48
|
+
export_format = getattr(args, 'export_format', 'json')
|
|
49
|
+
|
|
50
|
+
if export_format == 'csv':
|
|
51
|
+
return export_socket_site_to_csv(args, configuration)
|
|
52
|
+
else:
|
|
53
|
+
return export_socket_site_to_json(args, configuration)
|
|
54
|
+
|
|
55
|
+
|
|
11
56
|
def export_socket_site_to_json(args, configuration):
|
|
12
57
|
"""
|
|
13
58
|
Export consolidated site and socket data to JSON format
|
|
@@ -23,7 +68,8 @@ def export_socket_site_to_json(args, configuration):
|
|
|
23
68
|
try:
|
|
24
69
|
# Load CLI settings using the robust function
|
|
25
70
|
settings = load_cli_settings()
|
|
26
|
-
|
|
71
|
+
# Note: load_cli_settings() now returns embedded defaults if file cannot be loaded
|
|
72
|
+
|
|
27
73
|
account_id = getAccountID(args, configuration)
|
|
28
74
|
# Get account snapshot with siteIDs if provided
|
|
29
75
|
# Get siteIDs from args if provided (comma-separated string)
|
|
@@ -38,6 +84,26 @@ def export_socket_site_to_json(args, configuration):
|
|
|
38
84
|
## Call APIs to retrieve sites, interface and network ranges ##
|
|
39
85
|
###############################################################
|
|
40
86
|
snapshot_sites = getAccountSnapshot(args, configuration, account_id, site_ids)
|
|
87
|
+
|
|
88
|
+
# Check if no sites were found and handle gracefully
|
|
89
|
+
sites_list = snapshot_sites['data']['accountSnapshot']['sites']
|
|
90
|
+
if not sites_list or len(sites_list) == 0:
|
|
91
|
+
if site_ids:
|
|
92
|
+
# User provided specific site IDs but none were found
|
|
93
|
+
print(f"No sites found matching the provided site IDs: {', '.join(site_ids)}")
|
|
94
|
+
print("Please verify the site IDs are correct and that they exist in this account.")
|
|
95
|
+
return [{"success": False, "message": f"No sites found for the specified site IDs: {', '.join(site_ids)}", "sites_requested": site_ids}]
|
|
96
|
+
else:
|
|
97
|
+
# No site filter was provided but no sites exist at all
|
|
98
|
+
print("No sites found in this account.")
|
|
99
|
+
return [{"success": False, "message": "No sites found in account", "account_id": account_id}]
|
|
100
|
+
|
|
101
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
102
|
+
if site_ids:
|
|
103
|
+
print(f"Found {len(sites_list)} site(s) matching the provided site IDs")
|
|
104
|
+
else:
|
|
105
|
+
print(f"Found {len(sites_list)} site(s) in account")
|
|
106
|
+
|
|
41
107
|
entity_network_interfaces = getEntityLookup(args, configuration, account_id, "networkInterface")
|
|
42
108
|
entity_network_ranges = getEntityLookup(args, configuration, account_id, "siteRange")
|
|
43
109
|
entity_sites = getEntityLookup(args, configuration, account_id, "site")
|
|
@@ -48,6 +114,7 @@ def export_socket_site_to_json(args, configuration):
|
|
|
48
114
|
for snapshot_site in snapshot_sites['data']['accountSnapshot']['sites']:
|
|
49
115
|
site_id = snapshot_site.get('id')
|
|
50
116
|
connectionType = snapshot_site.get('infoSiteSnapshot', {}).get('connType', "")
|
|
117
|
+
# # Placeholder code to rename what the API returns if export should support cloud deployments
|
|
51
118
|
# if connectionType=="VSOCKET_VGX_AWS":
|
|
52
119
|
# connectionType = "SOCKET_AWS1500"
|
|
53
120
|
# elif connectionType=="VSOCKET_VGX_AZURE":
|
|
@@ -81,11 +148,19 @@ def export_socket_site_to_json(args, configuration):
|
|
|
81
148
|
cur_wan_interface['id'] = site_id+":"+ wan_ni.get('id', "")
|
|
82
149
|
else:
|
|
83
150
|
cur_wan_interface['id'] = site_id+":INT_"+ wan_ni.get('id', "")
|
|
151
|
+
# Format WAN interface index: INT_X for numeric values, keep as-is for non-numeric
|
|
152
|
+
wan_interface_id = wan_ni.get('id', "")
|
|
153
|
+
if isinstance(wan_interface_id, (int, str)) and str(wan_interface_id).isdigit():
|
|
154
|
+
cur_wan_interface['index'] = f"INT_{wan_interface_id}"
|
|
155
|
+
else:
|
|
156
|
+
cur_wan_interface['index'] = wan_interface_id
|
|
84
157
|
cur_wan_interface['name'] = wan_ni.get('name', "")
|
|
85
158
|
cur_wan_interface['upstream_bandwidth'] = wan_ni.get('upstreamBandwidth', 0)
|
|
86
159
|
cur_wan_interface['downstream_bandwidth'] = wan_ni.get('downstreamBandwidth', 0)
|
|
87
160
|
cur_wan_interface['dest_type'] = wan_ni.get('destType', "")
|
|
88
161
|
cur_wan_interface['role'] = role
|
|
162
|
+
# Not supported via API to be populated later when available
|
|
163
|
+
cur_wan_interface['precedence'] = "ACTIVE"
|
|
89
164
|
cur_site['wan_interfaces'].append(cur_wan_interface)
|
|
90
165
|
|
|
91
166
|
if site_id:
|
|
@@ -109,21 +184,27 @@ def export_socket_site_to_json(args, configuration):
|
|
|
109
184
|
lan_ni_subnet = str(lan_ni_helper_fields.get('subnet', ""))
|
|
110
185
|
ni_index = lan_ni_helper_fields.get('interfaceId', "")
|
|
111
186
|
ni_index = f"INT_{ni_index}" if isinstance(ni_index, (int, str)) and str(ni_index).isdigit() else ni_index
|
|
112
|
-
if cur_site_entry["connection_type"] in settings["default_socket_interface_map"] and ni_index in settings["default_socket_interface_map"][
|
|
187
|
+
if cur_site_entry["connection_type"] in settings["default_socket_interface_map"] and ni_index in settings["default_socket_interface_map"][cur_site_entry["connection_type"]]:
|
|
113
188
|
cur_native_range = cur_site_entry["native_range"]
|
|
114
189
|
cur_site_entry["native_range"]["interface_id"] = ni_interface_id
|
|
115
190
|
cur_site_entry["native_range"]["interface_name"] = ni_interface_name
|
|
116
191
|
cur_site_entry["native_range"]["subnet"] = lan_ni_subnet
|
|
117
192
|
cur_site_entry["native_range"]["index"] = ni_index
|
|
193
|
+
# Add entry to lan interfaces for default_lan
|
|
194
|
+
cur_site_entry['lan_interfaces'].append({"network_ranges": [],"default_lan":True})
|
|
118
195
|
else:
|
|
119
196
|
cur_lan_interface['id'] = ni_interface_id
|
|
120
197
|
cur_lan_interface['name'] = ni_interface_name
|
|
121
198
|
cur_lan_interface['index'] = ni_index
|
|
122
199
|
cur_lan_interface['dest_type'] = lan_ni_helper_fields.get('destType', "")
|
|
200
|
+
# temporarily add subnet to interface to be used later to flas native range_range
|
|
201
|
+
cur_lan_interface['subnet'] = lan_ni_subnet
|
|
123
202
|
cur_site_entry['lan_interfaces'].append(cur_lan_interface)
|
|
124
203
|
else:
|
|
125
204
|
if hasattr(args, 'verbose') and args.verbose:
|
|
126
|
-
|
|
205
|
+
ni_interface_name = lan_ni_helper_fields.get('interfaceName', "")
|
|
206
|
+
ni_interface_id = lan_ni_entity_data.get('id', "")
|
|
207
|
+
print(f"WARNING: Site {lan_ni_site_id} not found in snapshot data, skipping interface {ni_interface_name} ({ni_interface_id})")
|
|
127
208
|
|
|
128
209
|
#############################################################################
|
|
129
210
|
## Process entity lookup network ranges populating by network interface id ##
|
|
@@ -135,50 +216,93 @@ def export_socket_site_to_json(args, configuration):
|
|
|
135
216
|
nr_entity_data = range.get('entity', {})
|
|
136
217
|
nr_interface_name = str(nr_helper_fields.get('interfaceName', ""))
|
|
137
218
|
nr_site_id = str(nr_helper_fields.get('siteId', ""))
|
|
219
|
+
range_id = nr_entity_data.get('id', "")
|
|
220
|
+
|
|
138
221
|
nr_site_entry = next((site for site in processed_data['sites'] if site['id'] == nr_site_id), None)
|
|
139
222
|
if nr_site_entry:
|
|
140
|
-
nr_subnet = nr_helper_fields.get('subnet',
|
|
141
|
-
nr_vlan = nr_helper_fields.get('vlanTag',
|
|
223
|
+
nr_subnet = nr_helper_fields.get('subnet', None)
|
|
224
|
+
nr_vlan = nr_helper_fields.get('vlanTag', None)
|
|
142
225
|
nr_mdns_reflector = nr_helper_fields.get('mdnsReflector', False)
|
|
143
226
|
nr_dhcp_microsegmentation = nr_helper_fields.get('microsegmentation', False)
|
|
144
|
-
|
|
227
|
+
nr_interface_name = str(nr_helper_fields.get('interfaceName', ""))
|
|
228
|
+
range_name = nr_entity_data.get('name', nr_interface_name)
|
|
145
229
|
if range_name and " \\ " in range_name:
|
|
146
230
|
range_name = range_name.split(" \\ ").pop()
|
|
147
231
|
range_id = nr_entity_data.get('id', "")
|
|
148
|
-
nr_interface_name = str(nr_helper_fields.get('interfaceName', ""))
|
|
149
232
|
|
|
150
233
|
# the following fields are missing from the schema, populating blank fields in the interim
|
|
151
|
-
nr_dhcp_type = nr_helper_fields.get('XXXXX', "")
|
|
152
|
-
nr_ip_range = nr_helper_fields.get('XXXXX',
|
|
153
|
-
nr_relay_group_id = nr_helper_fields.get('XXXXX',
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
nr_translated_subnet = nr_helper_fields.get('XXXXX',
|
|
157
|
-
nr_internet_only = nr_helper_fields.get('XXXXX',
|
|
158
|
-
nr_local_ip = nr_helper_fields.get('XXXXX',
|
|
234
|
+
nr_dhcp_type = nr_helper_fields.get('XXXXX', "DHCP_DISABLED")
|
|
235
|
+
nr_ip_range = nr_helper_fields.get('XXXXX', None)
|
|
236
|
+
# nr_relay_group_id = nr_helper_fields.get('XXXXX', None)
|
|
237
|
+
nr_relay_group_name = nr_helper_fields.get('XXXXX', None)
|
|
238
|
+
nr_gateway = nr_helper_fields.get('XXXXX', None)
|
|
239
|
+
nr_translated_subnet = nr_helper_fields.get('XXXXX', None)
|
|
240
|
+
# nr_internet_only = nr_helper_fields.get('XXXXX', None)
|
|
241
|
+
nr_local_ip = nr_helper_fields.get('XXXXX', None)
|
|
242
|
+
nr_range_type = nr_helper_fields.get('XXXXX', None)
|
|
243
|
+
# Adding logic to pre-populate with default value
|
|
244
|
+
if nr_vlan!=None:
|
|
245
|
+
nr_range_type="VLAN"
|
|
246
|
+
else:
|
|
247
|
+
nr_range_type="Direct"
|
|
248
|
+
|
|
249
|
+
# Calculate local IP from subnet if --calculate-local-ip flag is set
|
|
250
|
+
if hasattr(args, 'calculate_local_ip') and args.calculate_local_ip and nr_subnet:
|
|
251
|
+
calculated_ip = calculateLocalIp(nr_subnet)
|
|
252
|
+
if calculated_ip:
|
|
253
|
+
nr_local_ip = calculated_ip
|
|
254
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
255
|
+
print(f" Calculated local IP for subnet {nr_subnet}: {calculated_ip}")
|
|
159
256
|
|
|
160
257
|
site_native_range = nr_site_entry.get('native_range', {}) if nr_site_entry else {}
|
|
161
258
|
|
|
162
259
|
if site_native_range.get("interface_name", "") == nr_interface_name:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
260
|
+
if range_name!="Native Range":
|
|
261
|
+
nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if 'default_lan' in lan_nic and lan_nic['default_lan']), None)
|
|
262
|
+
# print(f"checking range: {network_range_site_id} - {network_range_interface_name}")
|
|
263
|
+
if nr_lan_interface_entry:
|
|
264
|
+
cur_range = {}
|
|
265
|
+
cur_range['id'] = range_id
|
|
266
|
+
cur_range['name'] = range_name
|
|
267
|
+
cur_range['subnet'] = nr_subnet
|
|
268
|
+
cur_range['vlan'] = nr_vlan
|
|
269
|
+
cur_range['mdns_reflector'] = nr_mdns_reflector
|
|
270
|
+
## The folliowing fields are missing from the schema, populating blank fields in the interim
|
|
271
|
+
cur_range['gateway'] = nr_gateway
|
|
272
|
+
cur_range['range_type'] = nr_range_type
|
|
273
|
+
cur_range['translated_subnet'] = nr_translated_subnet
|
|
274
|
+
# # Not available to set for native_range via API today
|
|
275
|
+
# cur_range['internet_only'] = nr_internet_only
|
|
276
|
+
cur_range['local_ip'] = nr_local_ip # Use the calculated or original value
|
|
277
|
+
cur_range['dhcp_settings'] = {
|
|
278
|
+
'dhcp_type': nr_dhcp_type,
|
|
279
|
+
'ip_range': nr_ip_range,
|
|
280
|
+
'relay_group_id': None,
|
|
281
|
+
'relay_group_name': nr_relay_group_name,
|
|
282
|
+
'dhcp_microsegmentation': nr_dhcp_microsegmentation
|
|
283
|
+
}
|
|
284
|
+
nr_lan_interface_entry["network_ranges"].append(cur_range)
|
|
285
|
+
else:
|
|
286
|
+
site_native_range['range_name'] = range_name
|
|
287
|
+
site_native_range['range_id'] = range_id
|
|
288
|
+
site_native_range['vlan'] = nr_vlan
|
|
289
|
+
site_native_range['mdns_reflector'] = nr_mdns_reflector
|
|
290
|
+
# site_native_range['dhcp_microsegmentation'] = nr_dhcp_microsegmentation
|
|
291
|
+
site_native_range['gateway'] = nr_gateway
|
|
292
|
+
site_native_range['range_type'] = nr_range_type
|
|
293
|
+
site_native_range['translated_subnet'] = nr_translated_subnet
|
|
294
|
+
# # Not available to set for native_range via API today
|
|
295
|
+
# site_native_range['internet_only'] = nr_internet_only
|
|
296
|
+
site_native_range['local_ip'] = nr_local_ip
|
|
297
|
+
site_native_range['dhcp_settings'] = {
|
|
298
|
+
'dhcp_type': nr_dhcp_type,
|
|
299
|
+
'ip_range': nr_ip_range,
|
|
300
|
+
'relay_group_id': None,
|
|
301
|
+
'relay_group_name': nr_relay_group_name,
|
|
302
|
+
'dhcp_microsegmentation': nr_dhcp_microsegmentation
|
|
303
|
+
}
|
|
179
304
|
else:
|
|
180
|
-
nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if lan_nic['name'] == nr_interface_name), None)
|
|
181
|
-
# print(f"checking range: {network_range_site_id} - {network_range_interface_name}")
|
|
305
|
+
nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if ('default_lan' not in lan_nic or not lan_nic['default_lan']) and lan_nic['name'] == nr_interface_name), None)
|
|
182
306
|
if nr_lan_interface_entry:
|
|
183
307
|
cur_range = {}
|
|
184
308
|
cur_range['id'] = range_id
|
|
@@ -187,30 +311,62 @@ def export_socket_site_to_json(args, configuration):
|
|
|
187
311
|
cur_range['vlan'] = nr_vlan
|
|
188
312
|
cur_range['mdns_reflector'] = nr_mdns_reflector
|
|
189
313
|
## The folliowing fields are missing from the schema, populating blank fields in the interim
|
|
190
|
-
cur_range['gateway'] =
|
|
191
|
-
cur_range['range_type'] =
|
|
192
|
-
cur_range['translated_subnet'] =
|
|
193
|
-
|
|
194
|
-
cur_range['
|
|
314
|
+
cur_range['gateway'] = nr_gateway
|
|
315
|
+
cur_range['range_type'] = nr_range_type
|
|
316
|
+
cur_range['translated_subnet'] = nr_translated_subnet
|
|
317
|
+
# # Not available to set for native_range via API today
|
|
318
|
+
# cur_range['internet_only'] = nr_internet_only
|
|
319
|
+
cur_range['local_ip'] = nr_local_ip # Use the calculated or original value
|
|
195
320
|
cur_range['dhcp_settings'] = {
|
|
196
|
-
'dhcp_type':
|
|
197
|
-
'ip_range':
|
|
198
|
-
'relay_group_id':
|
|
321
|
+
'dhcp_type': nr_dhcp_type,
|
|
322
|
+
'ip_range': nr_ip_range,
|
|
323
|
+
'relay_group_id': None,
|
|
324
|
+
'relay_group_name': nr_relay_group_name,
|
|
199
325
|
'dhcp_microsegmentation': nr_dhcp_microsegmentation
|
|
200
326
|
}
|
|
327
|
+
# DEBUG
|
|
328
|
+
# print(json.dumps(nr_lan_interface_entry,indent=4,sort_keys=True))
|
|
329
|
+
# print("nr_subnet",nr_subnet)
|
|
330
|
+
# print('nr_lan_interface_entry["subnet"]='+nr_lan_interface_entry["subnet"])
|
|
331
|
+
# print(json.dumps(nr_lan_interface_entry,indent=4,sort_keys=True))
|
|
332
|
+
if "subnet" in nr_lan_interface_entry and nr_subnet==nr_lan_interface_entry["subnet"]:
|
|
333
|
+
cur_range['native_range'] = True
|
|
334
|
+
del nr_lan_interface_entry["subnet"]
|
|
335
|
+
|
|
201
336
|
nr_lan_interface_entry["network_ranges"].append(cur_range)
|
|
202
337
|
else:
|
|
203
|
-
|
|
204
|
-
|
|
338
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
339
|
+
print(f"Skipping range {nr_entity_data.get('id', '')}: site_id {nr_site_id} and {nr_interface_name} not found in ")
|
|
205
340
|
else:
|
|
206
341
|
if hasattr(args, 'verbose') and args.verbose:
|
|
207
342
|
print(f"Skipping range, site_id is unsupported for export {nr_site_id}")
|
|
208
343
|
|
|
209
|
-
# Handle
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
344
|
+
# Handle custom filename and timestamp
|
|
345
|
+
if hasattr(args, 'json_filename') and args.json_filename:
|
|
346
|
+
# User provided custom filename
|
|
347
|
+
base_filename = args.json_filename
|
|
348
|
+
# Remove .json extension if provided, we'll add it back
|
|
349
|
+
if base_filename.endswith('.json'):
|
|
350
|
+
base_filename = base_filename[:-5]
|
|
351
|
+
|
|
352
|
+
if hasattr(args, 'append_timestamp') and args.append_timestamp:
|
|
353
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
354
|
+
filename_template = f"{base_filename}_{timestamp}.json"
|
|
355
|
+
else:
|
|
356
|
+
filename_template = f"{base_filename}.json"
|
|
357
|
+
else:
|
|
358
|
+
# Use default filename template
|
|
359
|
+
if hasattr(args, 'append_timestamp') and args.append_timestamp:
|
|
360
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
361
|
+
filename_template = f"socket_sites_{{account_id}}_{timestamp}.json"
|
|
362
|
+
else:
|
|
363
|
+
filename_template = "socket_sites_{account_id}.json"
|
|
364
|
+
|
|
365
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
366
|
+
if hasattr(args, 'json_filename') and args.json_filename:
|
|
367
|
+
print(f"Using custom filename template: {filename_template}")
|
|
368
|
+
else:
|
|
369
|
+
print(f"Using default filename template: {filename_template}")
|
|
214
370
|
|
|
215
371
|
# Write the processed data to file using the general-purpose function
|
|
216
372
|
output_file = writeDataToFile(
|
|
@@ -253,6 +409,617 @@ def export_socket_site_to_json(args, configuration):
|
|
|
253
409
|
return [{"success": False, "error": str(e), "error_details": error_details}]
|
|
254
410
|
|
|
255
411
|
|
|
412
|
+
def export_socket_site_to_csv(args, configuration):
|
|
413
|
+
"""
|
|
414
|
+
Export consolidated site and socket data to CSV format
|
|
415
|
+
Creates main sites CSV and individual network ranges CSV files
|
|
416
|
+
"""
|
|
417
|
+
try:
|
|
418
|
+
# First get the JSON data using existing function
|
|
419
|
+
json_result = export_socket_site_to_json(args, configuration)
|
|
420
|
+
|
|
421
|
+
if not json_result or not json_result[0].get('success'):
|
|
422
|
+
return json_result
|
|
423
|
+
|
|
424
|
+
# Load the processed data from the JSON function
|
|
425
|
+
# We'll re-use the data processing logic but output to CSV instead
|
|
426
|
+
processed_data = get_processed_site_data(args, configuration)
|
|
427
|
+
|
|
428
|
+
if not processed_data or not processed_data.get('sites'):
|
|
429
|
+
return [{"success": False, "error": "No sites data found to export"}]
|
|
430
|
+
|
|
431
|
+
account_id = getAccountID(args, configuration)
|
|
432
|
+
output_files = []
|
|
433
|
+
|
|
434
|
+
# Export main sites CSV
|
|
435
|
+
sites_csv_file = export_sites_to_csv(processed_data['sites'], args, account_id)
|
|
436
|
+
output_files.append(sites_csv_file)
|
|
437
|
+
|
|
438
|
+
# Export individual network ranges CSV files for each site
|
|
439
|
+
for site in processed_data['sites']:
|
|
440
|
+
ranges_csv_file = export_network_ranges_to_csv(site, args, account_id)
|
|
441
|
+
if ranges_csv_file:
|
|
442
|
+
output_files.append(ranges_csv_file)
|
|
443
|
+
|
|
444
|
+
return [{"success": True, "output_files": output_files, "account_id": account_id}]
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
448
|
+
line_number = exc_traceback.tb_lineno
|
|
449
|
+
filename = exc_traceback.tb_frame.f_code.co_filename
|
|
450
|
+
function_name = exc_traceback.tb_frame.f_code.co_name
|
|
451
|
+
full_traceback = traceback.format_exc()
|
|
452
|
+
|
|
453
|
+
error_details = {
|
|
454
|
+
"error_type": exc_type.__name__,
|
|
455
|
+
"error_message": str(exc_value),
|
|
456
|
+
"line_number": line_number,
|
|
457
|
+
"function_name": function_name,
|
|
458
|
+
"filename": os.path.basename(filename),
|
|
459
|
+
"full_traceback": full_traceback
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
print(f"ERROR: {exc_type.__name__}: {str(exc_value)}")
|
|
463
|
+
print(f"Location: {os.path.basename(filename)}:{line_number} in {function_name}()")
|
|
464
|
+
print(f"Full traceback:\n{full_traceback}")
|
|
465
|
+
|
|
466
|
+
return [{"success": False, "error": str(e), "error_details": error_details}]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def get_processed_site_data(args, configuration):
|
|
470
|
+
"""
|
|
471
|
+
Get processed site data without writing to JSON file
|
|
472
|
+
Reuses the logic from export_socket_site_to_json but returns data instead
|
|
473
|
+
"""
|
|
474
|
+
processed_data = {'sites': []}
|
|
475
|
+
|
|
476
|
+
# Load CLI settings
|
|
477
|
+
settings = load_cli_settings()
|
|
478
|
+
# Note: load_cli_settings() now returns embedded defaults if file cannot be loaded
|
|
479
|
+
|
|
480
|
+
account_id = getAccountID(args, configuration)
|
|
481
|
+
|
|
482
|
+
# Get siteIDs from args if provided
|
|
483
|
+
site_ids = []
|
|
484
|
+
if hasattr(args, 'siteIDs') and args.siteIDs:
|
|
485
|
+
site_ids = [site_id.strip() for site_id in args.siteIDs.split(',') if site_id.strip()]
|
|
486
|
+
|
|
487
|
+
# Call APIs to retrieve sites, interface and network ranges
|
|
488
|
+
snapshot_sites = getAccountSnapshot(args, configuration, account_id, site_ids)
|
|
489
|
+
sites_list = snapshot_sites['data']['accountSnapshot']['sites']
|
|
490
|
+
|
|
491
|
+
if not sites_list or len(sites_list) == 0:
|
|
492
|
+
return processed_data
|
|
493
|
+
|
|
494
|
+
entity_network_interfaces = getEntityLookup(args, configuration, account_id, "networkInterface")
|
|
495
|
+
entity_network_ranges = getEntityLookup(args, configuration, account_id, "siteRange")
|
|
496
|
+
entity_sites = getEntityLookup(args, configuration, account_id, "site")
|
|
497
|
+
|
|
498
|
+
# Process sites (reuse existing logic)
|
|
499
|
+
for snapshot_site in snapshot_sites['data']['accountSnapshot']['sites']:
|
|
500
|
+
site_id = snapshot_site.get('id')
|
|
501
|
+
connectionType = snapshot_site.get('infoSiteSnapshot', {}).get('connType', "")
|
|
502
|
+
|
|
503
|
+
cur_site = {
|
|
504
|
+
'wan_interfaces': [],
|
|
505
|
+
'lan_interfaces': [],
|
|
506
|
+
'native_range': {}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if connectionType in settings["export_by_socket_type"]:
|
|
510
|
+
cur_site['id'] = site_id
|
|
511
|
+
cur_site['name'] = snapshot_site.get('infoSiteSnapshot', {}).get('name')
|
|
512
|
+
cur_site['description'] = snapshot_site.get('infoSiteSnapshot', {}).get('description')
|
|
513
|
+
cur_site['connection_type'] = connectionType
|
|
514
|
+
cur_site['type'] = snapshot_site.get('infoSiteSnapshot', {}).get('type')
|
|
515
|
+
cur_site = populateSiteLocationData(args, snapshot_site, cur_site)
|
|
516
|
+
|
|
517
|
+
# Process WAN interfaces
|
|
518
|
+
site_interfaces = snapshot_site.get('infoSiteSnapshot', {}).get('interfaces', [])
|
|
519
|
+
for wan_ni in site_interfaces:
|
|
520
|
+
cur_wan_interface = {}
|
|
521
|
+
role = wan_ni.get('wanRoleInterfaceInfo', "")
|
|
522
|
+
interfaceName = wan_ni.get('id', "")
|
|
523
|
+
if role is not None and role[0:3] == "wan":
|
|
524
|
+
if interfaceName[0:3] in ("WAN", "USB", "LTE"):
|
|
525
|
+
cur_wan_interface['id'] = site_id+":"+ wan_ni.get('id', "")
|
|
526
|
+
else:
|
|
527
|
+
cur_wan_interface['id'] = site_id+":INT_"+ wan_ni.get('id', "")
|
|
528
|
+
# Format WAN interface index: INT_X for numeric values, keep as-is for non-numeric
|
|
529
|
+
wan_interface_id = wan_ni.get('id', "")
|
|
530
|
+
if isinstance(wan_interface_id, (int, str)) and str(wan_interface_id).isdigit():
|
|
531
|
+
cur_wan_interface['index'] = f"INT_{wan_interface_id}"
|
|
532
|
+
else:
|
|
533
|
+
cur_wan_interface['index'] = wan_interface_id
|
|
534
|
+
cur_wan_interface['name'] = wan_ni.get('name', "")
|
|
535
|
+
cur_wan_interface['upstream_bandwidth'] = wan_ni.get('upstreamBandwidth', 0)
|
|
536
|
+
cur_wan_interface['downstream_bandwidth'] = wan_ni.get('downstreamBandwidth', 0)
|
|
537
|
+
cur_wan_interface['dest_type'] = wan_ni.get('destType', "")
|
|
538
|
+
cur_wan_interface['role'] = role
|
|
539
|
+
cur_wan_interface['precedence'] = "ACTIVE"
|
|
540
|
+
cur_site['wan_interfaces'].append(cur_wan_interface)
|
|
541
|
+
|
|
542
|
+
if site_id:
|
|
543
|
+
processed_data['sites'].append(cur_site)
|
|
544
|
+
|
|
545
|
+
# Process LAN interfaces (reuse existing logic)
|
|
546
|
+
for lan_ni in entity_network_interfaces:
|
|
547
|
+
lan_ni_helper_fields = lan_ni.get("helperFields", {})
|
|
548
|
+
lan_ni_entity_data = lan_ni.get('entity', {})
|
|
549
|
+
lan_ni_site_id = str(lan_ni_helper_fields.get('siteId', ""))
|
|
550
|
+
cur_site_entry = next((site for site in processed_data['sites'] if site['id'] == lan_ni_site_id), None)
|
|
551
|
+
if cur_site_entry:
|
|
552
|
+
cur_lan_interface = {'network_ranges': []}
|
|
553
|
+
ni_interface_id = lan_ni_entity_data.get('id', "")
|
|
554
|
+
ni_interface_name = lan_ni_helper_fields.get('interfaceName', "")
|
|
555
|
+
lan_ni_subnet = str(lan_ni_helper_fields.get('subnet', ""))
|
|
556
|
+
ni_index = lan_ni_helper_fields.get('interfaceId', "")
|
|
557
|
+
ni_index = f"INT_{ni_index}" if isinstance(ni_index, (int, str)) and str(ni_index).isdigit() else ni_index
|
|
558
|
+
|
|
559
|
+
if cur_site_entry["connection_type"] in settings["default_socket_interface_map"] and ni_index in settings["default_socket_interface_map"][cur_site_entry["connection_type"]]:
|
|
560
|
+
cur_site_entry["native_range"]["interface_id"] = ni_interface_id
|
|
561
|
+
cur_site_entry["native_range"]["interface_name"] = ni_interface_name
|
|
562
|
+
cur_site_entry["native_range"]["subnet"] = lan_ni_subnet
|
|
563
|
+
cur_site_entry["native_range"]["index"] = ni_index
|
|
564
|
+
cur_site_entry['lan_interfaces'].append({"network_ranges": [], "default_lan": True})
|
|
565
|
+
else:
|
|
566
|
+
cur_lan_interface['id'] = ni_interface_id
|
|
567
|
+
cur_lan_interface['name'] = ni_interface_name
|
|
568
|
+
cur_lan_interface['index'] = ni_index
|
|
569
|
+
cur_lan_interface['dest_type'] = lan_ni_helper_fields.get('destType', "")
|
|
570
|
+
cur_lan_interface['subnet'] = lan_ni_subnet
|
|
571
|
+
cur_site_entry['lan_interfaces'].append(cur_lan_interface)
|
|
572
|
+
|
|
573
|
+
# Process network ranges (reuse existing logic)
|
|
574
|
+
for range in entity_network_ranges:
|
|
575
|
+
nr_helper_fields = range.get("helperFields", {})
|
|
576
|
+
nr_entity_data = range.get('entity', {})
|
|
577
|
+
nr_interface_name = str(nr_helper_fields.get('interfaceName', ""))
|
|
578
|
+
nr_site_id = str(nr_helper_fields.get('siteId', ""))
|
|
579
|
+
range_id = nr_entity_data.get('id', "")
|
|
580
|
+
|
|
581
|
+
nr_site_entry = next((site for site in processed_data['sites'] if site['id'] == nr_site_id), None)
|
|
582
|
+
if nr_site_entry:
|
|
583
|
+
nr_subnet = nr_helper_fields.get('subnet', None)
|
|
584
|
+
nr_vlan = nr_helper_fields.get('vlanTag', None)
|
|
585
|
+
nr_mdns_reflector = nr_helper_fields.get('mdnsReflector', False)
|
|
586
|
+
nr_dhcp_microsegmentation = nr_helper_fields.get('microsegmentation', False)
|
|
587
|
+
range_name = nr_entity_data.get('name', nr_interface_name)
|
|
588
|
+
if range_name and " \\ " in range_name:
|
|
589
|
+
range_name = range_name.split(" \\ ").pop()
|
|
590
|
+
|
|
591
|
+
# Set defaults for missing fields
|
|
592
|
+
nr_dhcp_type = "DHCP_DISABLED"
|
|
593
|
+
nr_ip_range = None
|
|
594
|
+
nr_relay_group_name = None
|
|
595
|
+
nr_gateway = None
|
|
596
|
+
nr_translated_subnet = None
|
|
597
|
+
nr_local_ip = None
|
|
598
|
+
nr_range_type = "VLAN" if nr_vlan != None else "Direct"
|
|
599
|
+
|
|
600
|
+
# Calculate local IP if requested
|
|
601
|
+
if hasattr(args, 'calculate_local_ip') and args.calculate_local_ip and nr_subnet:
|
|
602
|
+
calculated_ip = calculateLocalIp(nr_subnet)
|
|
603
|
+
if calculated_ip:
|
|
604
|
+
nr_local_ip = calculated_ip
|
|
605
|
+
|
|
606
|
+
site_native_range = nr_site_entry.get('native_range', {})
|
|
607
|
+
|
|
608
|
+
if site_native_range.get("interface_name", "") == nr_interface_name:
|
|
609
|
+
if range_name != "Native Range":
|
|
610
|
+
nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if 'default_lan' in lan_nic and lan_nic['default_lan']), None)
|
|
611
|
+
if nr_lan_interface_entry:
|
|
612
|
+
cur_range = {
|
|
613
|
+
'id': range_id, 'name': range_name, 'subnet': nr_subnet, 'vlan': nr_vlan,
|
|
614
|
+
'mdns_reflector': nr_mdns_reflector, 'gateway': nr_gateway, 'range_type': nr_range_type,
|
|
615
|
+
'translated_subnet': nr_translated_subnet, 'local_ip': nr_local_ip,
|
|
616
|
+
'dhcp_settings': {
|
|
617
|
+
'dhcp_type': nr_dhcp_type, 'ip_range': nr_ip_range, 'relay_group_id': None,
|
|
618
|
+
'relay_group_name': nr_relay_group_name, 'dhcp_microsegmentation': nr_dhcp_microsegmentation
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
nr_lan_interface_entry["network_ranges"].append(cur_range)
|
|
622
|
+
else:
|
|
623
|
+
site_native_range['range_name'] = range_name
|
|
624
|
+
site_native_range['range_id'] = range_id
|
|
625
|
+
site_native_range['vlan'] = nr_vlan
|
|
626
|
+
site_native_range['mdns_reflector'] = nr_mdns_reflector
|
|
627
|
+
site_native_range['gateway'] = nr_gateway
|
|
628
|
+
site_native_range['range_type'] = nr_range_type
|
|
629
|
+
site_native_range['translated_subnet'] = nr_translated_subnet
|
|
630
|
+
site_native_range['local_ip'] = nr_local_ip
|
|
631
|
+
site_native_range['dhcp_settings'] = {
|
|
632
|
+
'dhcp_type': nr_dhcp_type, 'ip_range': nr_ip_range, 'relay_group_id': None,
|
|
633
|
+
'relay_group_name': nr_relay_group_name, 'dhcp_microsegmentation': nr_dhcp_microsegmentation
|
|
634
|
+
}
|
|
635
|
+
else:
|
|
636
|
+
nr_lan_interface_entry = next((lan_nic for lan_nic in nr_site_entry["lan_interfaces"] if ('default_lan' not in lan_nic or not lan_nic['default_lan']) and lan_nic['name'] == nr_interface_name), None)
|
|
637
|
+
if nr_lan_interface_entry:
|
|
638
|
+
cur_range = {
|
|
639
|
+
'id': range_id, 'name': range_name, 'subnet': nr_subnet, 'vlan': nr_vlan,
|
|
640
|
+
'mdns_reflector': nr_mdns_reflector, 'gateway': nr_gateway, 'range_type': nr_range_type,
|
|
641
|
+
'translated_subnet': nr_translated_subnet, 'local_ip': nr_local_ip,
|
|
642
|
+
'dhcp_settings': {
|
|
643
|
+
'dhcp_type': nr_dhcp_type, 'ip_range': nr_ip_range, 'relay_group_id': None,
|
|
644
|
+
'relay_group_name': nr_relay_group_name, 'dhcp_microsegmentation': nr_dhcp_microsegmentation
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if "subnet" in nr_lan_interface_entry and nr_subnet == nr_lan_interface_entry["subnet"]:
|
|
649
|
+
cur_range['native_range'] = True
|
|
650
|
+
del nr_lan_interface_entry["subnet"]
|
|
651
|
+
|
|
652
|
+
nr_lan_interface_entry["network_ranges"].append(cur_range)
|
|
653
|
+
|
|
654
|
+
return processed_data
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def export_sites_to_csv(sites, args, account_id):
|
|
658
|
+
"""
|
|
659
|
+
Export main sites data to CSV file in bulk-sites format
|
|
660
|
+
One row per WAN interface, site attributes only on first row per site
|
|
661
|
+
"""
|
|
662
|
+
# Handle custom filename and timestamp
|
|
663
|
+
if hasattr(args, 'csv_filename') and args.csv_filename:
|
|
664
|
+
base_filename = args.csv_filename
|
|
665
|
+
if base_filename.endswith('.csv'):
|
|
666
|
+
base_filename = base_filename[:-4]
|
|
667
|
+
|
|
668
|
+
if hasattr(args, 'append_timestamp') and args.append_timestamp:
|
|
669
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
670
|
+
filename_template = f"{base_filename}_{timestamp}.csv"
|
|
671
|
+
else:
|
|
672
|
+
filename_template = f"{base_filename}.csv"
|
|
673
|
+
else:
|
|
674
|
+
if hasattr(args, 'append_timestamp') and args.append_timestamp:
|
|
675
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
676
|
+
filename_template = f"socket_sites_{{account_id}}_{timestamp}.csv"
|
|
677
|
+
else:
|
|
678
|
+
filename_template = "socket_sites_{account_id}.csv"
|
|
679
|
+
|
|
680
|
+
# Replace account_id placeholder
|
|
681
|
+
filename = filename_template.format(account_id=account_id)
|
|
682
|
+
|
|
683
|
+
# Determine output directory
|
|
684
|
+
output_dir = getattr(args, 'output_directory', None)
|
|
685
|
+
if not output_dir:
|
|
686
|
+
output_dir = 'config_data'
|
|
687
|
+
if not os.path.isabs(output_dir):
|
|
688
|
+
output_dir = os.path.join(os.getcwd(), output_dir)
|
|
689
|
+
|
|
690
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
691
|
+
filepath = os.path.join(output_dir, filename)
|
|
692
|
+
|
|
693
|
+
# Define CSV headers matching bulk-sites.csv format
|
|
694
|
+
headers = [
|
|
695
|
+
'site_id',
|
|
696
|
+
'site_name',
|
|
697
|
+
'wan_interface_id',
|
|
698
|
+
'wan_interface_index',
|
|
699
|
+
'wan_interface_name',
|
|
700
|
+
'wan_upstream_bw',
|
|
701
|
+
'wan_downstream_bw',
|
|
702
|
+
'wan_role',
|
|
703
|
+
'wan_precedence',
|
|
704
|
+
'site_description',
|
|
705
|
+
'native_range_subnet',
|
|
706
|
+
'native_range_name',
|
|
707
|
+
'native_range_id',
|
|
708
|
+
'native_range_vlan',
|
|
709
|
+
'native_range_mdns_reflector',
|
|
710
|
+
'native_range_gateway',
|
|
711
|
+
'native_range_type',
|
|
712
|
+
'native_range_translated_subnet',
|
|
713
|
+
'native_range_interface_id',
|
|
714
|
+
'native_range_interface_index',
|
|
715
|
+
'native_range_interface_name',
|
|
716
|
+
'native_range_dhcp_type',
|
|
717
|
+
'native_range_dhcp_ip_range',
|
|
718
|
+
'native_range_dhcp_relay_group_id',
|
|
719
|
+
'native_range_dhcp_relay_group_name',
|
|
720
|
+
'native_range_dhcp_microsegmentation',
|
|
721
|
+
'native_range_local_ip',
|
|
722
|
+
'site_type',
|
|
723
|
+
'connection_type',
|
|
724
|
+
'site_location_address',
|
|
725
|
+
'site_location_city',
|
|
726
|
+
'site_location_country_code',
|
|
727
|
+
'site_location_state_code',
|
|
728
|
+
'site_location_timezone'
|
|
729
|
+
]
|
|
730
|
+
|
|
731
|
+
rows = []
|
|
732
|
+
total_interfaces = 0
|
|
733
|
+
|
|
734
|
+
for site in sites:
|
|
735
|
+
site_name = site.get('name', '')
|
|
736
|
+
wan_interfaces = site.get('wan_interfaces', [])
|
|
737
|
+
|
|
738
|
+
# Sort WAN interfaces to ensure 'wan_1' role appears first
|
|
739
|
+
# This organizes the CSV so wan_1 interfaces are on the first line for each site
|
|
740
|
+
def wan_interface_sort_key(interface):
|
|
741
|
+
role = interface.get('role', '').lower()
|
|
742
|
+
if role == 'wan_1':
|
|
743
|
+
return 0 # wan_1 comes first
|
|
744
|
+
elif role == 'wan_2':
|
|
745
|
+
return 1
|
|
746
|
+
elif role == 'wan_3':
|
|
747
|
+
return 2
|
|
748
|
+
elif role == 'wan_4':
|
|
749
|
+
return 3
|
|
750
|
+
else:
|
|
751
|
+
return 9 # Unknown/other roles come last
|
|
752
|
+
|
|
753
|
+
wan_interfaces.sort(key=wan_interface_sort_key)
|
|
754
|
+
|
|
755
|
+
# If no WAN interfaces, create one empty row for the site
|
|
756
|
+
if not wan_interfaces:
|
|
757
|
+
wan_interfaces = [{}] # Empty interface to ensure site is included
|
|
758
|
+
|
|
759
|
+
for idx, wan_interface in enumerate(wan_interfaces):
|
|
760
|
+
# Site-specific attributes only on first row per site
|
|
761
|
+
is_first_interface = (idx == 0)
|
|
762
|
+
|
|
763
|
+
# Calculate local IP from native range subnet if available
|
|
764
|
+
native_range = site.get('native_range', {})
|
|
765
|
+
native_subnet = native_range.get('subnet', '')
|
|
766
|
+
local_ip = ''
|
|
767
|
+
if native_subnet and hasattr(args, 'calculate_local_ip') and args.calculate_local_ip:
|
|
768
|
+
calculated_ip = calculateLocalIp(native_subnet)
|
|
769
|
+
if calculated_ip:
|
|
770
|
+
local_ip = calculated_ip
|
|
771
|
+
elif native_range.get('local_ip'):
|
|
772
|
+
local_ip = native_range.get('local_ip', '')
|
|
773
|
+
|
|
774
|
+
row = {
|
|
775
|
+
'site_id': site.get('id', '') if is_first_interface else '',
|
|
776
|
+
'site_name': site_name,
|
|
777
|
+
'wan_interface_id': wan_interface.get('id', '') if wan_interface else '',
|
|
778
|
+
'wan_interface_index': wan_interface.get('index', '') if wan_interface else '',
|
|
779
|
+
'wan_interface_name': wan_interface.get('name', '') if wan_interface else '',
|
|
780
|
+
'wan_upstream_bw': wan_interface.get('upstream_bandwidth', '') if wan_interface else '',
|
|
781
|
+
'wan_downstream_bw': wan_interface.get('downstream_bandwidth', '') if wan_interface else '',
|
|
782
|
+
'wan_role': wan_interface.get('role', '') if wan_interface else '',
|
|
783
|
+
'wan_precedence': wan_interface.get('precedence', '') if wan_interface else '',
|
|
784
|
+
|
|
785
|
+
# Site attributes - only populate on first interface row
|
|
786
|
+
'site_description': site.get('description', '') if is_first_interface else '',
|
|
787
|
+
'native_range_subnet': native_subnet if is_first_interface else '',
|
|
788
|
+
'native_range_name': native_range.get('range_name', '') if is_first_interface else '',
|
|
789
|
+
'native_range_id': native_range.get('range_id', '') if is_first_interface else '',
|
|
790
|
+
'native_range_vlan': native_range.get('vlan', '') if is_first_interface else '',
|
|
791
|
+
'native_range_mdns_reflector': str(native_range.get('mdns_reflector', '')).upper() if is_first_interface and native_range.get('mdns_reflector') != '' else '' if is_first_interface else '',
|
|
792
|
+
'native_range_gateway': native_range.get('gateway', '') if is_first_interface else '',
|
|
793
|
+
'native_range_type': native_range.get('range_type', '') if is_first_interface else '',
|
|
794
|
+
'native_range_translated_subnet': native_range.get('translated_subnet', '') if is_first_interface else '',
|
|
795
|
+
'native_range_interface_id': native_range.get('interface_id', '') if is_first_interface else '',
|
|
796
|
+
'native_range_interface_index': native_range.get('index', '') if is_first_interface else '',
|
|
797
|
+
'native_range_interface_name': native_range.get('interface_name', '') if is_first_interface else '',
|
|
798
|
+
'native_range_dhcp_type': native_range.get('dhcp_settings', {}).get('dhcp_type', '') if is_first_interface else '',
|
|
799
|
+
'native_range_dhcp_ip_range': native_range.get('dhcp_settings', {}).get('ip_range', '') if is_first_interface else '',
|
|
800
|
+
'native_range_dhcp_relay_group_id': native_range.get('dhcp_settings', {}).get('relay_group_id', '') if is_first_interface else '',
|
|
801
|
+
'native_range_dhcp_relay_group_name': native_range.get('dhcp_settings', {}).get('relay_group_name', '') if is_first_interface else '',
|
|
802
|
+
'native_range_dhcp_microsegmentation': str(native_range.get('dhcp_settings', {}).get('dhcp_microsegmentation', '')).upper() if is_first_interface and native_range.get('dhcp_settings', {}).get('dhcp_microsegmentation') != '' else '' if is_first_interface else '',
|
|
803
|
+
'native_range_local_ip': local_ip if is_first_interface else '',
|
|
804
|
+
'site_type': site.get('type', '') if is_first_interface else '',
|
|
805
|
+
'connection_type': site.get('connection_type', '') if is_first_interface else '',
|
|
806
|
+
'site_location_address': site.get('site_location', {}).get('address', '') if is_first_interface else '',
|
|
807
|
+
'site_location_city': site.get('site_location', {}).get('city', '') if is_first_interface else '',
|
|
808
|
+
'site_location_country_code': site.get('site_location', {}).get('countryCode', '') if is_first_interface else '',
|
|
809
|
+
'site_location_state_code': site.get('site_location', {}).get('stateCode', '') if is_first_interface else '',
|
|
810
|
+
'site_location_timezone': site.get('site_location', {}).get('timezone', '') if is_first_interface else ''
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
rows.append(row)
|
|
814
|
+
total_interfaces += 1 if wan_interface else 0
|
|
815
|
+
|
|
816
|
+
with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
|
|
817
|
+
writer = csv.DictWriter(csvfile, fieldnames=headers)
|
|
818
|
+
writer.writeheader()
|
|
819
|
+
writer.writerows(rows)
|
|
820
|
+
|
|
821
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
822
|
+
print(f"Exported {len(sites)} sites with {total_interfaces} WAN interfaces to {filepath}")
|
|
823
|
+
|
|
824
|
+
return filepath
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def export_network_ranges_to_csv(site, args, account_id):
|
|
828
|
+
"""
|
|
829
|
+
Export network ranges for a single site to CSV file
|
|
830
|
+
Structure: LAN interface as parent with network ranges as children
|
|
831
|
+
First row per interface contains interface details, subsequent rows contain only network range details
|
|
832
|
+
"""
|
|
833
|
+
site_name = site.get('name', '')
|
|
834
|
+
if not site_name:
|
|
835
|
+
return None
|
|
836
|
+
|
|
837
|
+
# Sanitize site name for filename
|
|
838
|
+
safe_site_name = "".join(c for c in site_name if c.isalnum() or c in ('-', '_')).rstrip()
|
|
839
|
+
|
|
840
|
+
# Determine output directory
|
|
841
|
+
output_dir = getattr(args, 'output_directory', None)
|
|
842
|
+
if not output_dir:
|
|
843
|
+
output_dir = 'config_data'
|
|
844
|
+
if not os.path.isabs(output_dir):
|
|
845
|
+
output_dir = os.path.join(os.getcwd(), output_dir)
|
|
846
|
+
|
|
847
|
+
# Create sites_config subdirectory with account ID
|
|
848
|
+
sites_config_dir = os.path.join(output_dir, f'sites_config_{account_id}')
|
|
849
|
+
os.makedirs(sites_config_dir, exist_ok=True)
|
|
850
|
+
|
|
851
|
+
filename = f"{safe_site_name}_network_ranges.csv"
|
|
852
|
+
filepath = os.path.join(sites_config_dir, filename)
|
|
853
|
+
|
|
854
|
+
# Define CSV headers - Reordered LAN interface columns first, then network range columns
|
|
855
|
+
headers = [
|
|
856
|
+
# LAN Interface columns (first 3 columns, is_native_range 4th, lan_interface_index 5th)
|
|
857
|
+
'lan_interface_id', 'lan_interface_name', 'lan_interface_dest_type', 'is_native_range', 'lan_interface_index',
|
|
858
|
+
# Network Range columns (populated on all rows)
|
|
859
|
+
'network_range_id', 'network_range_name', 'subnet', 'vlan', 'mdns_reflector',
|
|
860
|
+
'gateway', 'range_type', 'translated_subnet', 'local_ip',
|
|
861
|
+
'dhcp_type', 'dhcp_ip_range', 'dhcp_relay_group_id', 'dhcp_relay_group_name', 'dhcp_microsegmentation'
|
|
862
|
+
]
|
|
863
|
+
|
|
864
|
+
rows = []
|
|
865
|
+
|
|
866
|
+
# Get the native range subnet from the site to exclude it from detailed CSV
|
|
867
|
+
native_range_subnet = site.get('native_range', {}).get('subnet', '')
|
|
868
|
+
|
|
869
|
+
# Process default LAN interface (from native_range) - ONLY if it has additional networks
|
|
870
|
+
native_range = site.get('native_range', {})
|
|
871
|
+
native_range_index = native_range.get('index', '') # The default interface index from parent CSV
|
|
872
|
+
default_lan_interfaces = [lan_nic for lan_nic in site.get('lan_interfaces', []) if lan_nic.get('default_lan', False)]
|
|
873
|
+
|
|
874
|
+
if default_lan_interfaces:
|
|
875
|
+
for default_lan_interface in default_lan_interfaces:
|
|
876
|
+
interface_id = native_range.get('interface_id', '')
|
|
877
|
+
interface_name = native_range.get('interface_name', '')
|
|
878
|
+
interface_index = native_range.get('index', '') # Use actual interface index like INT_5
|
|
879
|
+
interface_dest_type = 'LAN'
|
|
880
|
+
|
|
881
|
+
network_ranges = default_lan_interface.get('network_ranges', [])
|
|
882
|
+
|
|
883
|
+
# Filter out the network range that matches the parent CSV native_range_subnet
|
|
884
|
+
filtered_ranges = [nr for nr in network_ranges if nr.get('subnet', '') != native_range_subnet]
|
|
885
|
+
|
|
886
|
+
# For default_lan interfaces, ONLY create entries if there are additional ranges
|
|
887
|
+
# Do NOT create an entry with is_native_range=TRUE since native range is managed at parent level
|
|
888
|
+
if filtered_ranges:
|
|
889
|
+
# Process filtered ranges - all marked as additional (non-native) ranges
|
|
890
|
+
for idx, network_range in enumerate(filtered_ranges):
|
|
891
|
+
# First row for this interface includes interface details
|
|
892
|
+
is_first_range = (idx == 0)
|
|
893
|
+
|
|
894
|
+
# For default_lan interfaces, all ranges are additional (is_native_range=FALSE)
|
|
895
|
+
# because the native range is already defined in the parent CSV
|
|
896
|
+
|
|
897
|
+
row = {
|
|
898
|
+
# LAN Interface details - for default LAN interfaces, don't populate interface ID, name, type since managed at parent level
|
|
899
|
+
'lan_interface_id': '', # Empty for default LAN interfaces (managed at parent level)
|
|
900
|
+
'lan_interface_name': '', # Empty for default LAN interfaces (managed at parent level)
|
|
901
|
+
'lan_interface_dest_type': '', # Empty for default LAN interfaces (managed at parent level)
|
|
902
|
+
'is_native_range': '', # Always empty for default LAN interfaces
|
|
903
|
+
'lan_interface_index': interface_index, # Populated for every row
|
|
904
|
+
|
|
905
|
+
# Network Range details (on all rows)
|
|
906
|
+
'network_range_id': network_range.get('id', ''),
|
|
907
|
+
'network_range_name': network_range.get('name', ''),
|
|
908
|
+
'subnet': network_range.get('subnet', ''),
|
|
909
|
+
'vlan': network_range.get('vlan', ''),
|
|
910
|
+
'mdns_reflector': str(network_range.get('mdns_reflector', False)).upper(),
|
|
911
|
+
'gateway': network_range.get('gateway', ''),
|
|
912
|
+
'range_type': network_range.get('range_type', ''),
|
|
913
|
+
'translated_subnet': network_range.get('translated_subnet', ''),
|
|
914
|
+
'local_ip': network_range.get('local_ip', ''),
|
|
915
|
+
'dhcp_type': network_range.get('dhcp_settings', {}).get('dhcp_type', ''),
|
|
916
|
+
'dhcp_ip_range': network_range.get('dhcp_settings', {}).get('ip_range', ''),
|
|
917
|
+
'dhcp_relay_group_id': network_range.get('dhcp_settings', {}).get('relay_group_id', ''),
|
|
918
|
+
'dhcp_relay_group_name': network_range.get('dhcp_settings', {}).get('relay_group_name', ''),
|
|
919
|
+
'dhcp_microsegmentation': str(network_range.get('dhcp_settings', {}).get('dhcp_microsegmentation', False)).upper()
|
|
920
|
+
}
|
|
921
|
+
rows.append(row)
|
|
922
|
+
|
|
923
|
+
# Process regular LAN interfaces and their network ranges
|
|
924
|
+
for lan_interface in site.get('lan_interfaces', []):
|
|
925
|
+
is_default_lan = lan_interface.get('default_lan', False)
|
|
926
|
+
|
|
927
|
+
# Skip default_lan interfaces (already processed above)
|
|
928
|
+
if is_default_lan:
|
|
929
|
+
continue
|
|
930
|
+
|
|
931
|
+
interface_id = lan_interface.get('id', '')
|
|
932
|
+
interface_name = lan_interface.get('name', '')
|
|
933
|
+
interface_index = lan_interface.get('index', '')
|
|
934
|
+
interface_dest_type = lan_interface.get('dest_type', '')
|
|
935
|
+
|
|
936
|
+
network_ranges = lan_interface.get('network_ranges', [])
|
|
937
|
+
|
|
938
|
+
# Filter out the network range that matches the parent CSV native_range_subnet
|
|
939
|
+
filtered_ranges = [nr for nr in network_ranges if nr.get('subnet', '') != native_range_subnet]
|
|
940
|
+
|
|
941
|
+
# If no filtered ranges, create at least one row with just the LAN interface info
|
|
942
|
+
# For regular LAN interfaces, mark as native range if this is the first/only interface
|
|
943
|
+
if not filtered_ranges:
|
|
944
|
+
row = {
|
|
945
|
+
# LAN Interface details - first 3 columns reordered, is_native_range 4th, lan_interface_index 5th
|
|
946
|
+
'lan_interface_id': interface_id,
|
|
947
|
+
'lan_interface_name': interface_name,
|
|
948
|
+
'lan_interface_dest_type': interface_dest_type,
|
|
949
|
+
'is_native_range': 'TRUE', # Mark as native range for non-default LAN interfaces with no additional ranges
|
|
950
|
+
'lan_interface_index': interface_index,
|
|
951
|
+
|
|
952
|
+
# Network Range details (empty since no additional ranges)
|
|
953
|
+
'network_range_id': '',
|
|
954
|
+
'network_range_name': '',
|
|
955
|
+
'subnet': '',
|
|
956
|
+
'vlan': '',
|
|
957
|
+
'mdns_reflector': '',
|
|
958
|
+
'gateway': '',
|
|
959
|
+
'range_type': '',
|
|
960
|
+
'translated_subnet': '',
|
|
961
|
+
'local_ip': '',
|
|
962
|
+
'dhcp_type': '',
|
|
963
|
+
'dhcp_ip_range': '',
|
|
964
|
+
'dhcp_relay_group_id': '',
|
|
965
|
+
'dhcp_relay_group_name': '',
|
|
966
|
+
'dhcp_microsegmentation': ''
|
|
967
|
+
}
|
|
968
|
+
rows.append(row)
|
|
969
|
+
else:
|
|
970
|
+
# Process filtered ranges as before
|
|
971
|
+
for idx, network_range in enumerate(filtered_ranges):
|
|
972
|
+
# First row for this interface includes interface details
|
|
973
|
+
is_first_range = (idx == 0)
|
|
974
|
+
|
|
975
|
+
# For non-default_lan interfaces, first range should be marked as is_native_range
|
|
976
|
+
is_native_range = is_first_range
|
|
977
|
+
|
|
978
|
+
row = {
|
|
979
|
+
# LAN Interface details - first 3 columns reordered, is_native_range 4th, lan_interface_index 5th
|
|
980
|
+
'lan_interface_id': interface_id if is_first_range else '',
|
|
981
|
+
'lan_interface_name': interface_name if is_first_range else '',
|
|
982
|
+
'lan_interface_dest_type': interface_dest_type if is_first_range else '',
|
|
983
|
+
'is_native_range': 'TRUE' if is_native_range else '',
|
|
984
|
+
'lan_interface_index': interface_index, # Populated for every row
|
|
985
|
+
|
|
986
|
+
# Network Range details (on all rows)
|
|
987
|
+
'network_range_id': network_range.get('id', ''),
|
|
988
|
+
'network_range_name': network_range.get('name', ''),
|
|
989
|
+
'subnet': network_range.get('subnet', ''),
|
|
990
|
+
'vlan': network_range.get('vlan', ''),
|
|
991
|
+
'mdns_reflector': str(network_range.get('mdns_reflector', False)).upper(),
|
|
992
|
+
'gateway': network_range.get('gateway', ''),
|
|
993
|
+
'range_type': network_range.get('range_type', ''),
|
|
994
|
+
'translated_subnet': network_range.get('translated_subnet', ''),
|
|
995
|
+
'local_ip': network_range.get('local_ip', ''),
|
|
996
|
+
'dhcp_type': network_range.get('dhcp_settings', {}).get('dhcp_type', ''),
|
|
997
|
+
'dhcp_ip_range': network_range.get('dhcp_settings', {}).get('ip_range', ''),
|
|
998
|
+
'dhcp_relay_group_id': network_range.get('dhcp_settings', {}).get('relay_group_id', ''),
|
|
999
|
+
'dhcp_relay_group_name': network_range.get('dhcp_settings', {}).get('relay_group_name', ''),
|
|
1000
|
+
'dhcp_microsegmentation': str(network_range.get('dhcp_settings', {}).get('dhcp_microsegmentation', False)).upper()
|
|
1001
|
+
}
|
|
1002
|
+
rows.append(row)
|
|
1003
|
+
|
|
1004
|
+
# If still no rows, it means the site only has the default LAN interface (managed at parent level)
|
|
1005
|
+
# In this case, create an empty CSV file - no entries needed since default interface is handled in parent CSV
|
|
1006
|
+
# This is correct behavior: sites with only default LAN interfaces should have empty site-level CSV files
|
|
1007
|
+
|
|
1008
|
+
# Always create file now (removed the early return)
|
|
1009
|
+
# if not rows:
|
|
1010
|
+
# return None
|
|
1011
|
+
|
|
1012
|
+
with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
|
|
1013
|
+
writer = csv.DictWriter(csvfile, fieldnames=headers)
|
|
1014
|
+
writer.writeheader()
|
|
1015
|
+
writer.writerows(rows)
|
|
1016
|
+
|
|
1017
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
1018
|
+
print(f"Exported {len(rows)} network ranges for site '{site_name}' to {filepath}")
|
|
1019
|
+
|
|
1020
|
+
return filepath
|
|
1021
|
+
|
|
1022
|
+
|
|
256
1023
|
##########################################################################
|
|
257
1024
|
########################### Helper functions #############################
|
|
258
1025
|
##########################################################################
|
|
@@ -277,13 +1044,16 @@ def populateSiteLocationData(args, site_data, cur_site):
|
|
|
277
1044
|
if hasattr(args, 'verbose') and args.verbose:
|
|
278
1045
|
print(f"Warning: Could not load site location data: {e}")
|
|
279
1046
|
|
|
1047
|
+
address = site_data.get('infoSiteSnapshot', {}).get('address')
|
|
1048
|
+
city = site_data.get('infoSiteSnapshot', {}).get('cityName')
|
|
1049
|
+
|
|
280
1050
|
## siteLocation attributes
|
|
281
1051
|
cur_site['site_location'] = {}
|
|
282
|
-
cur_site['site_location']['address'] = site_data.get('infoSiteSnapshot', {}).get('address')
|
|
283
|
-
cur_site['site_location']['city'] = site_data.get('infoSiteSnapshot', {}).get('cityName')
|
|
284
1052
|
cur_site['site_location']['stateName'] = site_data.get('infoSiteSnapshot', {}).get('countryStateName')
|
|
285
1053
|
cur_site['site_location']['countryCode'] = site_data.get('infoSiteSnapshot', {}).get('countryCode')
|
|
286
1054
|
cur_site['site_location']['countryName'] = site_data.get('infoSiteSnapshot', {}).get('countryName')
|
|
1055
|
+
cur_site['site_location']['address'] = address if address != "" else None
|
|
1056
|
+
cur_site['site_location']['city'] = city if city != "" else None
|
|
287
1057
|
|
|
288
1058
|
# Look up timezone and state code from location data
|
|
289
1059
|
country_name = cur_site['site_location']['countryName']
|
|
@@ -303,6 +1073,9 @@ def populateSiteLocationData(args, site_data, cur_site):
|
|
|
303
1073
|
# Look up location details
|
|
304
1074
|
location_data = site_location_data.get(lookup_key, {})
|
|
305
1075
|
|
|
1076
|
+
# Now that location_data is defined, we can set stateCode
|
|
1077
|
+
cur_site['site_location']['stateCode'] = location_data.get('stateCode', None)
|
|
1078
|
+
|
|
306
1079
|
if hasattr(args, 'verbose') and args.verbose:
|
|
307
1080
|
if location_data:
|
|
308
1081
|
print(f" Found location data: {location_data}")
|
|
@@ -313,8 +1086,7 @@ def populateSiteLocationData(args, site_data, cur_site):
|
|
|
313
1086
|
if similar_keys:
|
|
314
1087
|
print(f" Similar keys found: {similar_keys}")
|
|
315
1088
|
|
|
316
|
-
|
|
317
|
-
|
|
1089
|
+
|
|
318
1090
|
# Get timezone - always use the 0 element in the timezones array
|
|
319
1091
|
timezones = location_data.get('timezone', [])
|
|
320
1092
|
cur_site['site_location']['timezone'] = timezones[0] if timezones else None
|
|
@@ -372,6 +1144,7 @@ def getAccountSnapshot(args, configuration, account_id, site_ids=None):
|
|
|
372
1144
|
raise ValueError("Failed to retrieve snapshot data from API")
|
|
373
1145
|
|
|
374
1146
|
if not response or 'sites' not in response['data']['accountSnapshot'] or response['data']['accountSnapshot']['sites'] is None:
|
|
375
|
-
|
|
1147
|
+
# Instead of raising an exception, return an empty response structure
|
|
1148
|
+
response['data']['accountSnapshot']['sites'] = []
|
|
376
1149
|
|
|
377
1150
|
return response
|