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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Direct Terraform Import Script using Python
|
|
4
|
-
Imports socket sites, WAN interfaces, and network ranges directly using subprocess calls to terraform import
|
|
4
|
+
Imports socket sites, WAN interfaces, LAN interfaces and network ranges directly using subprocess calls to terraform import
|
|
5
5
|
Reads from JSON structure exported from Cato API
|
|
6
6
|
Adapted from scripts/import_if_rules_to_tfstate.py for CLI usage
|
|
7
7
|
"""
|
|
@@ -12,10 +12,12 @@ import sys
|
|
|
12
12
|
import re
|
|
13
13
|
import time
|
|
14
14
|
import glob
|
|
15
|
+
import csv
|
|
16
|
+
import os
|
|
17
|
+
import argparse
|
|
15
18
|
from pathlib import Path
|
|
16
19
|
from ..customLib import validate_terraform_environment
|
|
17
20
|
|
|
18
|
-
|
|
19
21
|
def load_json_data(json_file):
|
|
20
22
|
"""Load socket sites data from JSON file"""
|
|
21
23
|
try:
|
|
@@ -45,7 +47,8 @@ def sanitize_name_for_terraform(name):
|
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
def extract_socket_sites_data(sites_data):
|
|
48
|
-
"""Extract socket sites, WAN interfaces, and network ranges from the sites data
|
|
50
|
+
"""Extract socket sites, WAN interfaces, and network ranges from the sites data.
|
|
51
|
+
Supports both legacy (camelCase) and new (snake_case) JSON formats."""
|
|
49
52
|
sites = []
|
|
50
53
|
lan_interfaces = []
|
|
51
54
|
wan_interfaces = []
|
|
@@ -63,17 +66,16 @@ def extract_socket_sites_data(sites_data):
|
|
|
63
66
|
'address': site_location.get('address', '')
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
# Transform native_range data
|
|
69
|
+
# Transform native_range data (handle both shapes)
|
|
67
70
|
native_range = {
|
|
68
|
-
'native_network_range': site.get('native_network_range', ''),
|
|
69
|
-
'local_ip': site.get('local_ip', ''),
|
|
70
|
-
'translated_subnet': site.get('translated_subnet', ''),
|
|
71
|
-
'native_network_range_id': site.get('native_network_range_id', '')
|
|
71
|
+
'native_network_range': site.get('native_network_range', site.get('native_range', {}).get('subnet', '')),
|
|
72
|
+
'local_ip': site.get('local_ip', site.get('native_range', {}).get('local_ip', '')),
|
|
73
|
+
'translated_subnet': site.get('translated_subnet', site.get('native_range', {}).get('translated_subnet', '')),
|
|
74
|
+
'native_network_range_id': site.get('native_network_range_id', site.get('native_range', {}).get('range_id', ''))
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
dhcp_settings
|
|
76
|
-
if dhcp_settings and isinstance(dhcp_settings, dict) and dhcp_settings.get('dhcp_type'):
|
|
76
|
+
# Optional DHCP
|
|
77
|
+
dhcp_settings = site.get('dhcp_settings', site.get('native_range', {}).get('dhcp_settings'))
|
|
78
|
+
if dhcp_settings and isinstance(dhcp_settings, dict) and (dhcp_settings.get('dhcp_type') or dhcp_settings.get('ip_range') or dhcp_settings.get('relay_group_id')):
|
|
77
79
|
native_range['dhcp_settings'] = {
|
|
78
80
|
'dhcp_type': dhcp_settings.get('dhcp_type', ''),
|
|
79
81
|
'ip_range': dhcp_settings.get('ip_range', ''),
|
|
@@ -86,7 +88,7 @@ def extract_socket_sites_data(sites_data):
|
|
|
86
88
|
'id': site['id'],
|
|
87
89
|
'name': site['name'],
|
|
88
90
|
'description': site.get('description', ''),
|
|
89
|
-
'connection_type': site.get('connectionType', ''),
|
|
91
|
+
'connection_type': site.get('connectionType', site.get('connection_type', '')),
|
|
90
92
|
'site_type': site.get('type', ''),
|
|
91
93
|
'site_location': transformed_location,
|
|
92
94
|
'native_range': native_range
|
|
@@ -94,48 +96,133 @@ def extract_socket_sites_data(sites_data):
|
|
|
94
96
|
|
|
95
97
|
# Extract WAN interfaces for this site
|
|
96
98
|
for wan_interface in site.get('wan_interfaces', []):
|
|
97
|
-
|
|
99
|
+
# Accept both key styles
|
|
100
|
+
name = wan_interface.get('name')
|
|
101
|
+
wid = wan_interface.get('id')
|
|
102
|
+
index = wan_interface.get('index')
|
|
103
|
+
if wid and name and index:
|
|
104
|
+
# Apply the same index formatting logic as the Terraform module
|
|
105
|
+
try:
|
|
106
|
+
# If index is a number, format as INT_X
|
|
107
|
+
int(index)
|
|
108
|
+
formatted_index = f"INT_{index}"
|
|
109
|
+
except ValueError:
|
|
110
|
+
# If not a number, use as-is
|
|
111
|
+
formatted_index = index
|
|
112
|
+
|
|
98
113
|
wan_interfaces.append({
|
|
99
114
|
'site_id': site['id'],
|
|
100
115
|
'site_name': site['name'],
|
|
101
|
-
'interface_id':
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'
|
|
116
|
+
'interface_id': wid, # Full ID for actual import
|
|
117
|
+
'interface_index': formatted_index, # Formatted index for Terraform key
|
|
118
|
+
'name': name,
|
|
119
|
+
'upstream_bandwidth': wan_interface.get('upstreamBandwidth', wan_interface.get('upstream_bandwidth', 25)),
|
|
120
|
+
'downstream_bandwidth': wan_interface.get('downstreamBandwidth', wan_interface.get('downstream_bandwidth', 25)),
|
|
121
|
+
'dest_type': wan_interface.get('destType', wan_interface.get('dest_type', 'CATO')),
|
|
122
|
+
'role': wan_interface.get('role', 'wan_1'),
|
|
107
123
|
'precedence': 'ACTIVE'
|
|
108
124
|
})
|
|
109
125
|
|
|
110
|
-
# Extract network ranges for this site
|
|
126
|
+
# Extract LAN interfaces and network ranges for this site
|
|
127
|
+
# Process LAN interfaces first to determine which ones will be created as separate resources
|
|
128
|
+
valid_lan_interfaces = []
|
|
129
|
+
|
|
111
130
|
for lan_interface in site.get('lan_interfaces', []):
|
|
112
|
-
interface_id = lan_interface.get('id',
|
|
113
|
-
interface_name = lan_interface.get('name',
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
interface_id = lan_interface.get('id', None)
|
|
132
|
+
interface_name = lan_interface.get('name', None)
|
|
133
|
+
interface_index = lan_interface.get('index', None)
|
|
134
|
+
is_default_lan = lan_interface.get('default_lan', False)
|
|
135
|
+
|
|
136
|
+
# If this is a default_lan interface, get interface info from native_range
|
|
137
|
+
if is_default_lan:
|
|
138
|
+
native_range = site.get('native_range', {})
|
|
139
|
+
interface_id = native_range.get('interface_id')
|
|
140
|
+
interface_name = native_range.get('interface_name')
|
|
141
|
+
interface_index = native_range.get('index')
|
|
142
|
+
|
|
143
|
+
# Check if this interface matches the site's default native range (should be excluded)
|
|
144
|
+
native_range_data = site.get('native_range', {})
|
|
145
|
+
is_site_default_native = (
|
|
146
|
+
is_default_lan and
|
|
147
|
+
interface_index == native_range_data.get('index') and
|
|
148
|
+
interface_name == native_range_data.get('interface_name')
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Only include interfaces that:
|
|
152
|
+
# 1. Have both interface_index and interface_id
|
|
153
|
+
# 2. Are NOT the site's default native range interface
|
|
154
|
+
will_create_interface = (interface_index is not None and interface_id and not is_site_default_native)
|
|
122
155
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
'subnet'
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
if will_create_interface:
|
|
157
|
+
# For default_lan interfaces, get additional info from the interface itself or native_range
|
|
158
|
+
subnet = lan_interface.get('subnet', '')
|
|
159
|
+
local_ip = lan_interface.get('local_ip', '')
|
|
160
|
+
|
|
161
|
+
# If this is a default_lan interface and we don't have subnet/local_ip, get from native_range
|
|
162
|
+
if is_default_lan:
|
|
163
|
+
native_range_data = site.get('native_range', {})
|
|
164
|
+
if not subnet:
|
|
165
|
+
subnet = native_range_data.get('subnet', '')
|
|
166
|
+
if not local_ip:
|
|
167
|
+
local_ip = native_range_data.get('local_ip', '')
|
|
168
|
+
|
|
169
|
+
lan_interfaces.append({
|
|
170
|
+
'site_id': site['id'],
|
|
171
|
+
'id': interface_id,
|
|
172
|
+
'index': interface_index,
|
|
173
|
+
'name': interface_name,
|
|
174
|
+
'dest_type': lan_interface.get('destType', lan_interface.get('dest_type', 'LAN')),
|
|
175
|
+
'subnet': subnet,
|
|
176
|
+
'local_ip': local_ip,
|
|
177
|
+
'role': interface_index or interface_name,
|
|
178
|
+
'site_name': site.get('name', ''),
|
|
179
|
+
'is_default_lan': is_default_lan # Add this for debugging
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
valid_lan_interfaces.append((interface_index, interface_name, interface_id, is_default_lan))
|
|
183
|
+
|
|
184
|
+
# Process network ranges for interfaces that will be created as separate resources
|
|
185
|
+
# OR for any interface that has network ranges (including virtual interfaces)
|
|
186
|
+
has_network_ranges = len(lan_interface.get('network_ranges', [])) > 0
|
|
187
|
+
should_process_ranges = will_create_interface or has_network_ranges
|
|
188
|
+
|
|
189
|
+
if should_process_ranges:
|
|
190
|
+
for network_range in lan_interface.get('network_ranges', []):
|
|
191
|
+
subnet = network_range.get('subnet')
|
|
192
|
+
if network_range.get('id') and subnet:
|
|
193
|
+
# Skip native ranges - these are managed at the site level, not as separate network range resources
|
|
194
|
+
is_native_range = network_range.get('native_range', False)
|
|
195
|
+
if is_native_range:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# Use the same interface info logic for network ranges
|
|
199
|
+
range_interface_id = interface_id
|
|
200
|
+
range_interface_index = interface_index
|
|
201
|
+
range_interface_name = interface_name
|
|
202
|
+
|
|
203
|
+
# If this is a default_lan interface, use native_range info
|
|
204
|
+
if is_default_lan:
|
|
205
|
+
native_range = site.get('native_range', {})
|
|
206
|
+
range_interface_id = native_range.get('interface_id')
|
|
207
|
+
range_interface_name = native_range.get('interface_name')
|
|
208
|
+
range_interface_index = native_range.get('index')
|
|
209
|
+
|
|
210
|
+
# print(f"Processing Network Range subnet={subnet}, interface_index={range_interface_index}, network_range_id={network_range['id']}, will_create_interface={will_create_interface}")
|
|
211
|
+
network_ranges.append({
|
|
212
|
+
'site_id': site['id'],
|
|
213
|
+
'site_name': site['name'],
|
|
214
|
+
'interface_id': range_interface_id, # Use actual interface ID, not index
|
|
215
|
+
'interface_index': range_interface_index, # Also pass interface index separately
|
|
216
|
+
'interface_name': range_interface_name,
|
|
217
|
+
'network_range_id': network_range['id'],
|
|
218
|
+
'name': network_range.get('rangeName', network_range.get('name', '')),
|
|
219
|
+
'subnet': subnet,
|
|
220
|
+
'vlan_tag': network_range.get('vlanTag', network_range.get('vlan', '')),
|
|
221
|
+
'range_type': 'VLAN' if (network_range.get('vlanTag') or network_range.get('vlan')) else 'Native',
|
|
222
|
+
'microsegmentation': network_range.get('microsegmentation', False)
|
|
223
|
+
})
|
|
137
224
|
|
|
138
|
-
return sites, wan_interfaces, network_ranges
|
|
225
|
+
return sites, wan_interfaces, lan_interfaces, network_ranges
|
|
139
226
|
|
|
140
227
|
|
|
141
228
|
def run_terraform_import(resource_address, resource_id, timeout=60, verbose=False):
|
|
@@ -184,343 +271,6 @@ def run_terraform_import(resource_address, resource_id, timeout=60, verbose=Fals
|
|
|
184
271
|
print(f"Unexpected error for {resource_address}: {e}")
|
|
185
272
|
return False, "", str(e)
|
|
186
273
|
|
|
187
|
-
|
|
188
|
-
def find_rule_index(rules, rule_name):
|
|
189
|
-
"""Find rule index by name."""
|
|
190
|
-
for index, rule in enumerate(rules):
|
|
191
|
-
if rule['name'] == rule_name:
|
|
192
|
-
return index
|
|
193
|
-
return None
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def import_sections(sections, module_name, resource_type,
|
|
197
|
-
resource_name="sections", verbose=False):
|
|
198
|
-
"""Import all sections"""
|
|
199
|
-
print("\nStarting section imports...")
|
|
200
|
-
total_sections = len(sections)
|
|
201
|
-
successful_imports = 0
|
|
202
|
-
failed_imports = 0
|
|
203
|
-
|
|
204
|
-
for i, section in enumerate(sections):
|
|
205
|
-
section_id = section['section_id']
|
|
206
|
-
section_name = section['section_name']
|
|
207
|
-
section_index = section['section_index']
|
|
208
|
-
resource_address = f'{module_name}.{resource_type}.{resource_name}["{section_name}"]'
|
|
209
|
-
print(f"\n[{i+1}/{total_sections}] Section: {section_name} (index: {section_index})")
|
|
210
|
-
|
|
211
|
-
# For sections, we use the section name as the ID since that's how Cato identifies them
|
|
212
|
-
success, stdout, stderr = run_terraform_import(resource_address, section_id, verbose=verbose)
|
|
213
|
-
|
|
214
|
-
if success:
|
|
215
|
-
successful_imports += 1
|
|
216
|
-
else:
|
|
217
|
-
failed_imports += 1
|
|
218
|
-
|
|
219
|
-
print(f"\nSection Import Summary: {successful_imports} successful, {failed_imports} failed")
|
|
220
|
-
return successful_imports, failed_imports
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def import_rules(rules, module_name, verbose=False,
|
|
224
|
-
resource_type="cato_if_rule", resource_name="rules",
|
|
225
|
-
batch_size=10, delay_between_batches=2, auto_approve=False):
|
|
226
|
-
"""Import all rules in batches"""
|
|
227
|
-
print("\nStarting rule imports...")
|
|
228
|
-
successful_imports = 0
|
|
229
|
-
failed_imports = 0
|
|
230
|
-
total_rules = len(rules)
|
|
231
|
-
|
|
232
|
-
for i, rule in enumerate(rules):
|
|
233
|
-
rule_id = rule['id']
|
|
234
|
-
rule_name = rule['name']
|
|
235
|
-
rule_index = find_rule_index(rules, rule_name)
|
|
236
|
-
terraform_key = sanitize_name_for_terraform(rule_name)
|
|
237
|
-
|
|
238
|
-
# Use array index syntax instead of rule ID
|
|
239
|
-
resource_address = f'{module_name}.{resource_type}.{resource_name}["{str(rule_name)}"]'
|
|
240
|
-
print(f"\n[{i+1}/{total_rules}] Rule: {rule_name} (index: {rule_index})")
|
|
241
|
-
|
|
242
|
-
success, stdout, stderr = run_terraform_import(resource_address, rule_id, verbose=verbose)
|
|
243
|
-
|
|
244
|
-
if success:
|
|
245
|
-
successful_imports += 1
|
|
246
|
-
else:
|
|
247
|
-
failed_imports += 1
|
|
248
|
-
|
|
249
|
-
# Ask user if they want to continue on failure (unless auto-approved)
|
|
250
|
-
if failed_imports <= 3 and not auto_approve: # Only prompt for first few failures
|
|
251
|
-
response = input(f"\nContinue with remaining imports? (y/n): ").lower()
|
|
252
|
-
if response == 'n':
|
|
253
|
-
print("Import process stopped by user.")
|
|
254
|
-
break
|
|
255
|
-
|
|
256
|
-
# Delay between batches
|
|
257
|
-
if (i + 1) % batch_size == 0 and i < total_rules - 1:
|
|
258
|
-
print(f"\n Batch complete. Waiting {delay_between_batches}s before next batch...")
|
|
259
|
-
time.sleep(delay_between_batches)
|
|
260
|
-
|
|
261
|
-
print(f"\n Rule Import Summary: {successful_imports} successful, {failed_imports} failed")
|
|
262
|
-
return successful_imports, failed_imports
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def import_if_rules_to_tf(args, configuration):
|
|
266
|
-
"""Main function to orchestrate the import process"""
|
|
267
|
-
try:
|
|
268
|
-
print(" Terraform Import Tool - Cato IFW Rules & Sections")
|
|
269
|
-
print("=" * 60)
|
|
270
|
-
|
|
271
|
-
# Load data
|
|
272
|
-
print(f" Loading data from {args.json_file}...")
|
|
273
|
-
policy_data = load_json_data(args.json_file)
|
|
274
|
-
|
|
275
|
-
# Extract rules and sections
|
|
276
|
-
rules, sections = extract_rules_and_sections(policy_data)
|
|
277
|
-
|
|
278
|
-
if hasattr(args, 'verbose') and args.verbose:
|
|
279
|
-
print(f"section_ids: {json.dumps(policy_data.get('section_ids', {}), indent=2)}")
|
|
280
|
-
|
|
281
|
-
print(f" Found {len(rules)} rules")
|
|
282
|
-
print(f" Found {len(sections)} sections")
|
|
283
|
-
|
|
284
|
-
if not rules and not sections:
|
|
285
|
-
print(" No rules or sections found. Exiting.")
|
|
286
|
-
return [{"success": False, "error": "No rules or sections found"}]
|
|
287
|
-
|
|
288
|
-
# Validate Terraform environment before proceeding
|
|
289
|
-
validate_terraform_environment(args.module_name, verbose=args.verbose)
|
|
290
|
-
|
|
291
|
-
# Ask for confirmation (unless auto-approved)
|
|
292
|
-
if not args.rules_only and not args.sections_only:
|
|
293
|
-
print(f"\n Ready to import {len(sections)} sections and {len(rules)} rules.")
|
|
294
|
-
elif args.rules_only:
|
|
295
|
-
print(f"\n Ready to import {len(rules)} rules only.")
|
|
296
|
-
elif args.sections_only:
|
|
297
|
-
print(f"\n Ready to import {len(sections)} sections only.")
|
|
298
|
-
|
|
299
|
-
if hasattr(args, 'auto_approve') and args.auto_approve:
|
|
300
|
-
print("\nAuto-approve enabled, proceeding with import...")
|
|
301
|
-
else:
|
|
302
|
-
confirm = input(f"\nProceed with import? (y/n): ").lower()
|
|
303
|
-
if confirm != 'y':
|
|
304
|
-
print("Import cancelled.")
|
|
305
|
-
return [{"success": False, "error": "Import cancelled by user"}]
|
|
306
|
-
|
|
307
|
-
total_successful = 0
|
|
308
|
-
total_failed = 0
|
|
309
|
-
|
|
310
|
-
# Import sections first (if not skipped)
|
|
311
|
-
if not args.rules_only and sections:
|
|
312
|
-
successful, failed = import_sections(sections, module_name=args.module_name, resource_type="cato_if_section", verbose=args.verbose)
|
|
313
|
-
total_successful += successful
|
|
314
|
-
total_failed += failed
|
|
315
|
-
|
|
316
|
-
# Import rules (if not skipped)
|
|
317
|
-
if not args.sections_only and rules:
|
|
318
|
-
successful, failed = import_rules(rules, module_name=args.module_name,
|
|
319
|
-
verbose=args.verbose, batch_size=args.batch_size,
|
|
320
|
-
delay_between_batches=args.delay,
|
|
321
|
-
auto_approve=getattr(args, 'auto_approve', False))
|
|
322
|
-
total_successful += successful
|
|
323
|
-
total_failed += failed
|
|
324
|
-
|
|
325
|
-
# Final summary
|
|
326
|
-
print("\n" + "=" * 60)
|
|
327
|
-
print(" FINAL IMPORT SUMMARY")
|
|
328
|
-
print("=" * 60)
|
|
329
|
-
print(f" Total successful imports: {total_successful}")
|
|
330
|
-
print(f" Total failed imports: {total_failed}")
|
|
331
|
-
print(f" Overall success rate: {(total_successful / (total_successful + total_failed) * 100):.1f}%" if (total_successful + total_failed) > 0 else "N/A")
|
|
332
|
-
print("\n Import process completed!")
|
|
333
|
-
|
|
334
|
-
return [{
|
|
335
|
-
"success": True,
|
|
336
|
-
"total_successful": total_successful,
|
|
337
|
-
"total_failed": total_failed,
|
|
338
|
-
"module_name": args.module_name
|
|
339
|
-
}]
|
|
340
|
-
|
|
341
|
-
except Exception as e:
|
|
342
|
-
print(f"ERROR: {str(e)}")
|
|
343
|
-
return [{"success": False, "error": str(e)}]
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
def load_wf_json_data(json_file):
|
|
347
|
-
"""Load WAN Firewall data from JSON file"""
|
|
348
|
-
try:
|
|
349
|
-
with open(json_file, 'r') as f:
|
|
350
|
-
data = json.load(f)
|
|
351
|
-
return data['data']['policy']['wanFirewall']['policy']
|
|
352
|
-
except FileNotFoundError:
|
|
353
|
-
print(f"Error: JSON file '{json_file}' not found")
|
|
354
|
-
sys.exit(1)
|
|
355
|
-
except json.JSONDecodeError as e:
|
|
356
|
-
print(f"Error: Invalid JSON in '{json_file}': {e}")
|
|
357
|
-
sys.exit(1)
|
|
358
|
-
except KeyError as e:
|
|
359
|
-
print(f"Error: Expected JSON structure not found in '{json_file}': {e}")
|
|
360
|
-
sys.exit(1)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def import_wf_sections(sections, module_name, verbose=False,
|
|
364
|
-
resource_type="cato_wf_section", resource_name="sections"):
|
|
365
|
-
"""Import all WAN Firewall sections"""
|
|
366
|
-
print("\nStarting WAN Firewall section imports...")
|
|
367
|
-
total_sections = len(sections)
|
|
368
|
-
successful_imports = 0
|
|
369
|
-
failed_imports = 0
|
|
370
|
-
|
|
371
|
-
for i, section in enumerate(sections):
|
|
372
|
-
section_id = section['section_id']
|
|
373
|
-
section_name = section['section_name']
|
|
374
|
-
section_index = section['section_index']
|
|
375
|
-
# Add module. prefix if not present
|
|
376
|
-
if not module_name.startswith('module.'):
|
|
377
|
-
module_name = f'module.{module_name}'
|
|
378
|
-
resource_address = f'{module_name}.{resource_type}.{resource_name}["{section_name}"]'
|
|
379
|
-
print(f"\n[{i+1}/{total_sections}] Section: {section_name} (index: {section_index})")
|
|
380
|
-
|
|
381
|
-
# For sections, we use the section name as the ID since that's how Cato identifies them
|
|
382
|
-
success, stdout, stderr = run_terraform_import(resource_address, section_id, verbose=verbose)
|
|
383
|
-
|
|
384
|
-
if success:
|
|
385
|
-
successful_imports += 1
|
|
386
|
-
else:
|
|
387
|
-
failed_imports += 1
|
|
388
|
-
|
|
389
|
-
print(f"\nWAN Firewall Section Import Summary: {successful_imports} successful, {failed_imports} failed")
|
|
390
|
-
return successful_imports, failed_imports
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def import_wf_rules(rules, module_name, verbose=False,
|
|
394
|
-
resource_type="cato_wf_rule", resource_name="rules",
|
|
395
|
-
batch_size=10, delay_between_batches=2, auto_approve=False):
|
|
396
|
-
"""Import all WAN Firewall rules in batches"""
|
|
397
|
-
print("\nStarting WAN Firewall rule imports...")
|
|
398
|
-
successful_imports = 0
|
|
399
|
-
failed_imports = 0
|
|
400
|
-
total_rules = len(rules)
|
|
401
|
-
|
|
402
|
-
for i, rule in enumerate(rules):
|
|
403
|
-
rule_id = rule['id']
|
|
404
|
-
rule_name = rule['name']
|
|
405
|
-
rule_index = find_rule_index(rules, rule_name)
|
|
406
|
-
terraform_key = sanitize_name_for_terraform(rule_name)
|
|
407
|
-
|
|
408
|
-
# Add module. prefix if not present
|
|
409
|
-
if not module_name.startswith('module.'):
|
|
410
|
-
module_name = f'module.{module_name}'
|
|
411
|
-
|
|
412
|
-
# Use array index syntax instead of rule ID
|
|
413
|
-
resource_address = f'{module_name}.{resource_type}.{resource_name}["{str(rule_name)}"]'
|
|
414
|
-
print(f"\n[{i+1}/{total_rules}] Rule: {rule_name} (index: {rule_index})")
|
|
415
|
-
|
|
416
|
-
success, stdout, stderr = run_terraform_import(resource_address, rule_id, verbose=verbose)
|
|
417
|
-
|
|
418
|
-
if success:
|
|
419
|
-
successful_imports += 1
|
|
420
|
-
else:
|
|
421
|
-
failed_imports += 1
|
|
422
|
-
|
|
423
|
-
# Ask user if they want to continue on failure (unless auto-approved)
|
|
424
|
-
if failed_imports <= 3 and not auto_approve: # Only prompt for first few failures
|
|
425
|
-
response = input(f"\nContinue with remaining imports? (y/n): ").lower()
|
|
426
|
-
if response == 'n':
|
|
427
|
-
print("Import process stopped by user.")
|
|
428
|
-
break
|
|
429
|
-
|
|
430
|
-
# Delay between batches
|
|
431
|
-
if (i + 1) % batch_size == 0 and i < total_rules - 1:
|
|
432
|
-
print(f"\n Batch complete. Waiting {delay_between_batches}s before next batch...")
|
|
433
|
-
time.sleep(delay_between_batches)
|
|
434
|
-
|
|
435
|
-
print(f"\nWAN Firewall Rule Import Summary: {successful_imports} successful, {failed_imports} failed")
|
|
436
|
-
return successful_imports, failed_imports
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def import_wf_rules_to_tf(args, configuration):
|
|
440
|
-
"""Main function to orchestrate the WAN Firewall import process"""
|
|
441
|
-
try:
|
|
442
|
-
print(" Terraform Import Tool - Cato WF Rules & Sections")
|
|
443
|
-
print("=" * 60)
|
|
444
|
-
|
|
445
|
-
# Load data
|
|
446
|
-
print(f" Loading data from {args.json_file}...")
|
|
447
|
-
policy_data = load_wf_json_data(args.json_file)
|
|
448
|
-
|
|
449
|
-
# Extract rules and sections
|
|
450
|
-
rules, sections = extract_rules_and_sections(policy_data)
|
|
451
|
-
|
|
452
|
-
if hasattr(args, 'verbose') and args.verbose:
|
|
453
|
-
print(f"section_ids: {json.dumps(policy_data.get('section_ids', {}), indent=2)}")
|
|
454
|
-
|
|
455
|
-
print(f" Found {len(rules)} rules")
|
|
456
|
-
print(f" Found {len(sections)} sections")
|
|
457
|
-
|
|
458
|
-
if not rules and not sections:
|
|
459
|
-
print(" No rules or sections found. Exiting.")
|
|
460
|
-
return [{"success": False, "error": "No rules or sections found"}]
|
|
461
|
-
|
|
462
|
-
# Add module. prefix if not present
|
|
463
|
-
module_name = args.module_name
|
|
464
|
-
if not module_name.startswith('module.'):
|
|
465
|
-
module_name = f'module.{module_name}'
|
|
466
|
-
# Validate Terraform environment before proceeding
|
|
467
|
-
validate_terraform_environment(module_name, verbose=args.verbose)
|
|
468
|
-
|
|
469
|
-
# Ask for confirmation (unless auto-approved)
|
|
470
|
-
if not args.rules_only and not args.sections_only:
|
|
471
|
-
print(f"\n Ready to import {len(sections)} sections and {len(rules)} rules.")
|
|
472
|
-
elif args.rules_only:
|
|
473
|
-
print(f"\n Ready to import {len(rules)} rules only.")
|
|
474
|
-
elif args.sections_only:
|
|
475
|
-
print(f"\n Ready to import {len(sections)} sections only.")
|
|
476
|
-
|
|
477
|
-
if hasattr(args, 'auto_approve') and args.auto_approve:
|
|
478
|
-
print("\nAuto-approve enabled, proceeding with import...")
|
|
479
|
-
else:
|
|
480
|
-
confirm = input(f"\nProceed with import? (y/n): ").lower()
|
|
481
|
-
if confirm != 'y':
|
|
482
|
-
print("Import cancelled.")
|
|
483
|
-
return [{"success": False, "error": "Import cancelled by user"}]
|
|
484
|
-
|
|
485
|
-
total_successful = 0
|
|
486
|
-
total_failed = 0
|
|
487
|
-
|
|
488
|
-
# Import sections first (if not skipped)
|
|
489
|
-
if not args.rules_only and sections:
|
|
490
|
-
successful, failed = import_wf_sections(sections, module_name=args.module_name, verbose=args.verbose)
|
|
491
|
-
total_successful += successful
|
|
492
|
-
total_failed += failed
|
|
493
|
-
|
|
494
|
-
# Import rules (if not skipped)
|
|
495
|
-
if not args.sections_only and rules:
|
|
496
|
-
successful, failed = import_wf_rules(rules, module_name=args.module_name,
|
|
497
|
-
verbose=args.verbose, batch_size=args.batch_size,
|
|
498
|
-
delay_between_batches=args.delay,
|
|
499
|
-
auto_approve=getattr(args, 'auto_approve', False))
|
|
500
|
-
total_successful += successful
|
|
501
|
-
total_failed += failed
|
|
502
|
-
|
|
503
|
-
# Final summary
|
|
504
|
-
print("\n" + "=" * 60)
|
|
505
|
-
print(" FINAL IMPORT SUMMARY")
|
|
506
|
-
print("=" * 60)
|
|
507
|
-
print(f" Total successful imports: {total_successful}")
|
|
508
|
-
print(f" Total failed imports: {total_failed}")
|
|
509
|
-
print(f" Overall success rate: {(total_successful / (total_successful + total_failed) * 100):.1f}%" if (total_successful + total_failed) > 0 else "N/A")
|
|
510
|
-
print("\n Import process completed!")
|
|
511
|
-
|
|
512
|
-
return [{
|
|
513
|
-
"success": True,
|
|
514
|
-
"total_successful": total_successful,
|
|
515
|
-
"total_failed": total_failed,
|
|
516
|
-
"module_name": args.module_name
|
|
517
|
-
}]
|
|
518
|
-
|
|
519
|
-
except Exception as e:
|
|
520
|
-
print(f"ERROR: {str(e)}")
|
|
521
|
-
return [{"success": False, "error": str(e)}]
|
|
522
|
-
|
|
523
|
-
|
|
524
274
|
def import_socket_sites(sites, module_name, verbose=False,
|
|
525
275
|
resource_type="cato_socket_site", resource_name="socket-site",
|
|
526
276
|
batch_size=10, delay_between_batches=2, auto_approve=False):
|
|
@@ -567,7 +317,7 @@ def import_socket_sites(sites, module_name, verbose=False,
|
|
|
567
317
|
|
|
568
318
|
|
|
569
319
|
def import_wan_interfaces(wan_interfaces, module_name, verbose=False,
|
|
570
|
-
resource_type="cato_wan_interface", resource_name="
|
|
320
|
+
resource_type="cato_wan_interface", resource_name="wan",
|
|
571
321
|
batch_size=10, delay_between_batches=2, auto_approve=False):
|
|
572
322
|
"""Import all WAN interfaces in batches"""
|
|
573
323
|
print("\nStarting WAN interface imports...")
|
|
@@ -585,18 +335,17 @@ def import_wan_interfaces(wan_interfaces, module_name, verbose=False,
|
|
|
585
335
|
if not module_name.startswith('module.'):
|
|
586
336
|
module_name = f'module.{module_name}'
|
|
587
337
|
|
|
588
|
-
#
|
|
589
|
-
#
|
|
590
|
-
|
|
591
|
-
resource_address = f'{module_name}.module.socket-site["{site_name}"].cato_wan_interface.wan["{
|
|
338
|
+
# In the module, cato_wan_interface.wan is now keyed by interface_index, which we
|
|
339
|
+
# format from the JSON "index" field. Use interface_index as the key.
|
|
340
|
+
wan_key = interface.get('interface_index', interface_id) # Use formatted index, fallback to ID
|
|
341
|
+
resource_address = f'{module_name}.module.socket-site["{site_name}"].cato_wan_interface.wan["{wan_key}"]'
|
|
592
342
|
|
|
593
|
-
# WAN
|
|
594
|
-
# Check if interface_id is already in the correct format (contains ':')
|
|
343
|
+
# WAN import id must be "site_id:interface_part"
|
|
595
344
|
if ':' in interface_id:
|
|
596
|
-
import_id = interface_id
|
|
345
|
+
import_id = interface_id
|
|
597
346
|
else:
|
|
598
|
-
import_id = f"{site_id}:{interface_id}"
|
|
599
|
-
print(f"\n[{i+1}/{total_interfaces}] WAN Interface: {interface_name} on {site_name} (
|
|
347
|
+
import_id = f"{site_id}:{interface_id}"
|
|
348
|
+
print(f"\n[{i+1}/{total_interfaces}] WAN Interface: {interface_name} on {site_name} (Key: {wan_key})")
|
|
600
349
|
|
|
601
350
|
success, stdout, stderr = run_terraform_import(resource_address, import_id, verbose=verbose)
|
|
602
351
|
|
|
@@ -605,14 +354,12 @@ def import_wan_interfaces(wan_interfaces, module_name, verbose=False,
|
|
|
605
354
|
else:
|
|
606
355
|
failed_imports += 1
|
|
607
356
|
|
|
608
|
-
|
|
609
|
-
if failed_imports <= 3 and not auto_approve: # Only prompt for first few failures
|
|
357
|
+
if failed_imports <= 3 and not auto_approve:
|
|
610
358
|
response = input(f"\nContinue with remaining imports? (y/n): ").lower()
|
|
611
359
|
if response == 'n':
|
|
612
360
|
print("Import process stopped by user.")
|
|
613
361
|
break
|
|
614
362
|
|
|
615
|
-
# Delay between batches
|
|
616
363
|
if (i + 1) % batch_size == 0 and i < total_interfaces - 1:
|
|
617
364
|
print(f"\n Batch complete. Waiting {delay_between_batches}s before next batch...")
|
|
618
365
|
time.sleep(delay_between_batches)
|
|
@@ -621,17 +368,17 @@ def import_wan_interfaces(wan_interfaces, module_name, verbose=False,
|
|
|
621
368
|
return successful_imports, failed_imports
|
|
622
369
|
|
|
623
370
|
def import_lan_interfaces(lan_interfaces, module_name, verbose=False,
|
|
624
|
-
resource_type="cato_lan_interface", resource_name="
|
|
371
|
+
resource_type="cato_lan_interface", resource_name="interface",
|
|
625
372
|
batch_size=10, delay_between_batches=2, auto_approve=False):
|
|
626
|
-
"""Import all
|
|
627
|
-
print("\nStarting
|
|
373
|
+
"""Import all LAN interfaces in batches"""
|
|
374
|
+
print("\nStarting LAN interface imports...")
|
|
628
375
|
successful_imports = 0
|
|
629
376
|
failed_imports = 0
|
|
630
377
|
total_interfaces = len(lan_interfaces)
|
|
631
378
|
|
|
632
379
|
for i, interface in enumerate(lan_interfaces):
|
|
633
380
|
site_id = interface['site_id']
|
|
634
|
-
interface_id = interface['id']
|
|
381
|
+
interface_id = interface['id'] # Actual interface ID from CSV
|
|
635
382
|
interface_index = interface['index']
|
|
636
383
|
interface_name = interface['name']
|
|
637
384
|
site_name = interface['site_name']
|
|
@@ -640,13 +387,23 @@ def import_lan_interfaces(lan_interfaces, module_name, verbose=False,
|
|
|
640
387
|
if not module_name.startswith('module.'):
|
|
641
388
|
module_name = f'module.{module_name}'
|
|
642
389
|
|
|
643
|
-
#
|
|
644
|
-
#
|
|
645
|
-
|
|
646
|
-
|
|
390
|
+
# Updated addressing to use interface_index-based indexing for resource addressing:
|
|
391
|
+
# module.sites.module.socket-site[site].module.lan_interfaces[interface_index].cato_lan_interface.interface[0]
|
|
392
|
+
# Apply the same index formatting logic as the Terraform module
|
|
393
|
+
try:
|
|
394
|
+
# If index is a number, format as INT_X
|
|
395
|
+
int(interface_index)
|
|
396
|
+
formatted_index = f"INT_{interface_index}"
|
|
397
|
+
except (ValueError, TypeError):
|
|
398
|
+
# If not a number or None, use as-is
|
|
399
|
+
formatted_index = interface_index if interface_index else interface_id
|
|
400
|
+
|
|
401
|
+
# The resource address uses interface_index for both the module key and the for_each key
|
|
402
|
+
resource_address = f'{module_name}.module.socket-site["{site_name}"].module.lan_interfaces["{formatted_index}"].cato_lan_interface.interface["{formatted_index}"]'
|
|
647
403
|
|
|
648
|
-
print(f"\n[{i+1}/{total_interfaces}] LAN Interface: {interface_name} on {site_name} (ID: {interface_id})")
|
|
404
|
+
print(f"\n[{i+1}/{total_interfaces}] LAN Interface: {interface_name} on {site_name} (Index: {interface_index}, ID: {interface_id})")
|
|
649
405
|
|
|
406
|
+
# Use the actual interface_id for importing, not the formatted index
|
|
650
407
|
success, stdout, stderr = run_terraform_import(resource_address, interface_id, verbose=verbose)
|
|
651
408
|
|
|
652
409
|
if success:
|
|
@@ -654,23 +411,21 @@ def import_lan_interfaces(lan_interfaces, module_name, verbose=False,
|
|
|
654
411
|
else:
|
|
655
412
|
failed_imports += 1
|
|
656
413
|
|
|
657
|
-
|
|
658
|
-
if failed_imports <= 3 and not auto_approve: # Only prompt for first few failures
|
|
414
|
+
if failed_imports <= 3 and not auto_approve:
|
|
659
415
|
response = input(f"\nContinue with remaining imports? (y/n): ").lower()
|
|
660
416
|
if response == 'n':
|
|
661
417
|
print("Import process stopped by user.")
|
|
662
418
|
break
|
|
663
419
|
|
|
664
|
-
# Delay between batches
|
|
665
420
|
if (i + 1) % batch_size == 0 and i < total_interfaces - 1:
|
|
666
421
|
print(f"\n Batch complete. Waiting {delay_between_batches}s before next batch...")
|
|
667
422
|
time.sleep(delay_between_batches)
|
|
668
423
|
|
|
669
|
-
print(f"\
|
|
424
|
+
print(f"\nLAN Interface Import Summary: {successful_imports} successful, {failed_imports} failed")
|
|
670
425
|
return successful_imports, failed_imports
|
|
671
426
|
|
|
672
|
-
def import_network_ranges(network_ranges, module_name, verbose=False,
|
|
673
|
-
resource_type="cato_network_range", resource_name="
|
|
427
|
+
def import_network_ranges(network_ranges, lan_interfaces, module_name, verbose=False,
|
|
428
|
+
resource_type="cato_network_range", resource_name="network_range",
|
|
674
429
|
batch_size=10, delay_between_batches=2, auto_approve=False):
|
|
675
430
|
"""Import all network ranges in batches"""
|
|
676
431
|
print("\nStarting network range imports...")
|
|
@@ -683,26 +438,50 @@ def import_network_ranges(network_ranges, module_name, verbose=False,
|
|
|
683
438
|
range_name = network_range['name']
|
|
684
439
|
site_name = network_range['site_name']
|
|
685
440
|
subnet = network_range['subnet']
|
|
441
|
+
interface_index = network_range['interface_index']
|
|
686
442
|
|
|
687
443
|
# Add module. prefix if not present
|
|
688
444
|
if not module_name.startswith('module.'):
|
|
689
445
|
module_name = f'module.{module_name}'
|
|
690
446
|
|
|
691
|
-
#
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
#
|
|
698
|
-
|
|
699
|
-
|
|
447
|
+
# Apply the same index formatting logic as the Terraform module
|
|
448
|
+
try:
|
|
449
|
+
# If index is a number, format as INT_X
|
|
450
|
+
int(interface_index)
|
|
451
|
+
formatted_index = f"INT_{interface_index}"
|
|
452
|
+
except (ValueError, TypeError):
|
|
453
|
+
# If not a number or None, use as-is (fallback to interface_id if needed)
|
|
454
|
+
formatted_index = interface_index if interface_index else network_range['interface_id']
|
|
455
|
+
|
|
456
|
+
# Determine if this is a default interface range (connected to native/default interface)
|
|
457
|
+
# Check if this network range has a corresponding LAN interface resource that was extracted
|
|
458
|
+
# If no LAN interface was created for this interface_index, it's a default interface range
|
|
459
|
+
lan_interface_exists = any(
|
|
460
|
+
lan['index'] == interface_index for lan in lan_interfaces
|
|
461
|
+
if lan['site_name'] == site_name
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
is_default_interface = not lan_interface_exists
|
|
465
|
+
|
|
466
|
+
# Generate the correct key format based on whether this is a default interface range
|
|
467
|
+
sanitized_range_name = range_name.replace(" ", "_")
|
|
468
|
+
if is_default_interface:
|
|
469
|
+
# For default interface ranges, use "DEFAULT" as the prefix to match socket module logic
|
|
470
|
+
range_key = f"DEFAULT-{sanitized_range_name}"
|
|
700
471
|
else:
|
|
701
|
-
|
|
472
|
+
# For regular LAN interface ranges, use the formatted interface index
|
|
473
|
+
range_key = f"{formatted_index}-{sanitized_range_name}"
|
|
702
474
|
|
|
703
|
-
|
|
475
|
+
# Determine the correct resource addressing based on whether this is a default interface
|
|
476
|
+
if is_default_interface:
|
|
477
|
+
# Default interface network ranges are addressed directly under the socket-site module
|
|
478
|
+
resource_address = f'{module_name}.module.socket-site["{site_name}"].cato_network_range.default_interface_ranges["{range_key}"]'
|
|
479
|
+
else:
|
|
480
|
+
# Regular interface network ranges go through the lan_interfaces module
|
|
481
|
+
resource_address = f'{module_name}.module.socket-site["{site_name}"].module.lan_interfaces["{formatted_index}"].module.network_ranges.module.network_range["{range_key}"].cato_network_range.no_dhcp[0]'
|
|
704
482
|
|
|
705
|
-
print(f"\n[{i+1}/{total_ranges}] Network Range: {range_name} - {subnet} ({network_range_id}) on {site_name}
|
|
483
|
+
print(f"\n[{i+1}/{total_ranges}] Network Range: {range_name} - {subnet} ({network_range_id}) on {site_name}")
|
|
484
|
+
print(f" Resource Address: {resource_address}")
|
|
706
485
|
|
|
707
486
|
success, stdout, stderr = run_terraform_import(resource_address, network_range_id, verbose=verbose)
|
|
708
487
|
|
|
@@ -831,21 +610,81 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
831
610
|
print(" Terraform Import Tool - Cato Socket Sites, WAN Interfaces & Network Ranges")
|
|
832
611
|
print("=" * 80)
|
|
833
612
|
|
|
834
|
-
#
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
613
|
+
# Determine data source and load data
|
|
614
|
+
data_type = getattr(args, 'data_type', None)
|
|
615
|
+
json_file = getattr(args, 'json_file', None) or getattr(args, 'json_file_legacy', None)
|
|
616
|
+
csv_file = getattr(args, 'csv_file', None)
|
|
617
|
+
csv_folder = getattr(args, 'csv_folder', None)
|
|
618
|
+
|
|
619
|
+
# Validate input arguments
|
|
620
|
+
if data_type:
|
|
621
|
+
# If data type is explicitly specified, validate corresponding file arguments
|
|
622
|
+
if data_type == 'json' and not json_file:
|
|
623
|
+
raise ValueError("--data-type json requires --json-file argument")
|
|
624
|
+
elif data_type == 'csv' and not csv_file:
|
|
625
|
+
raise ValueError("--data-type csv requires --csv-file argument")
|
|
626
|
+
elif data_type == 'json' and csv_file:
|
|
627
|
+
raise ValueError("Cannot specify both --data-type json and --csv-file")
|
|
628
|
+
elif data_type == 'csv' and json_file:
|
|
629
|
+
raise ValueError("Cannot specify both --data-type csv and --json-file")
|
|
630
|
+
else:
|
|
631
|
+
# Auto-detect data type if not specified
|
|
632
|
+
if json_file and csv_file:
|
|
633
|
+
raise ValueError("Cannot specify both JSON and CSV files. Use --data-type to specify which format to use.")
|
|
634
|
+
elif json_file and json_file.endswith('.json'):
|
|
635
|
+
data_type = 'json'
|
|
636
|
+
print(" Auto-detected JSON format from file extension")
|
|
637
|
+
elif csv_file and csv_file.endswith('.csv'):
|
|
638
|
+
data_type = 'csv'
|
|
639
|
+
print(" Auto-detected CSV format from file extension")
|
|
640
|
+
elif json_file:
|
|
641
|
+
data_type = 'json'
|
|
642
|
+
print(" Auto-detected JSON format")
|
|
643
|
+
elif csv_file:
|
|
644
|
+
data_type = 'csv'
|
|
645
|
+
print(" Auto-detected CSV format")
|
|
646
|
+
else:
|
|
647
|
+
print("\nERROR: No input file specified.\n")
|
|
648
|
+
print("Please provide either:")
|
|
649
|
+
print(" JSON: --json-file <file> or positional argument")
|
|
650
|
+
print(" CSV: --csv-file <file> [--csv-folder <folder>]\n")
|
|
651
|
+
print("Use 'catocli import socket_sites_to_tf -h' for detailed help and examples.")
|
|
652
|
+
raise ValueError("No input file provided")
|
|
653
|
+
|
|
654
|
+
# Validate inputs based on data type
|
|
655
|
+
if data_type == 'json':
|
|
656
|
+
if not json_file:
|
|
657
|
+
raise ValueError("JSON import requires --json-file or positional json_file argument")
|
|
658
|
+
print(f" Loading JSON data from {json_file}...")
|
|
659
|
+
sites_data = load_json_data(json_file)
|
|
660
|
+
elif data_type == 'csv':
|
|
661
|
+
if not csv_file:
|
|
662
|
+
raise ValueError("CSV import requires --csv-file argument")
|
|
663
|
+
print(f" Loading CSV data from {csv_file}...")
|
|
664
|
+
if csv_folder:
|
|
665
|
+
print(f" Loading network ranges from {csv_folder}...")
|
|
666
|
+
sites_data = load_csv_data(csv_file, csv_folder)
|
|
667
|
+
else:
|
|
668
|
+
raise ValueError(f"Unsupported data type: {data_type}")
|
|
840
669
|
|
|
670
|
+
# Extract sites, WAN interfaces, LAN interfaces, and network ranges
|
|
671
|
+
sites, wan_interfaces, lan_interfaces, network_ranges = extract_socket_sites_data(sites_data)
|
|
841
672
|
if hasattr(args, 'verbose') and args.verbose:
|
|
673
|
+
print("\n==================== DEBUG =====================\n")
|
|
674
|
+
print("sites",json.dumps( sites, indent=2))
|
|
675
|
+
print("wan_interfaces",json.dumps( wan_interfaces, indent=2))
|
|
676
|
+
print("lan_interfaces",json.dumps( lan_interfaces, indent=2))
|
|
677
|
+
print("network_ranges",json.dumps( network_ranges, indent=2))
|
|
678
|
+
print("\n==================== DEBUG =====================\n")
|
|
842
679
|
print(f"\nExtracted data summary:")
|
|
843
680
|
print(f" Sites: {len(sites)}")
|
|
844
681
|
print(f" WAN Interfaces: {len(wan_interfaces)}")
|
|
682
|
+
print(f" LAN Interfaces: {len(lan_interfaces)}")
|
|
845
683
|
print(f" Network Ranges: {len(network_ranges)}")
|
|
846
684
|
|
|
847
685
|
print(f" Found {len(sites)} sites")
|
|
848
686
|
print(f" Found {len(wan_interfaces)} WAN interfaces")
|
|
687
|
+
print(f" Found {len(lan_interfaces)} LAN interfaces")
|
|
849
688
|
print(f" Found {len(network_ranges)} network ranges")
|
|
850
689
|
|
|
851
690
|
if not sites and not wan_interfaces and not network_ranges:
|
|
@@ -878,16 +717,25 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
878
717
|
validate_terraform_environment(module_name, verbose=args.verbose)
|
|
879
718
|
|
|
880
719
|
# Ask for confirmation (unless auto-approved)
|
|
720
|
+
# Determine which categories to import based on flags
|
|
721
|
+
sites_only = getattr(args, 'sites_only', False)
|
|
722
|
+
wan_only = getattr(args, 'wan_interfaces_only', False)
|
|
723
|
+
lan_only = getattr(args, 'lan_interfaces_only', False)
|
|
724
|
+
ranges_only = getattr(args, 'network_ranges_only', False)
|
|
725
|
+
|
|
881
726
|
import_summary = []
|
|
882
|
-
if not
|
|
727
|
+
if not (sites_only or wan_only or lan_only or ranges_only):
|
|
883
728
|
import_summary.append(f"{len(sites)} sites")
|
|
884
729
|
import_summary.append(f"{len(wan_interfaces)} WAN interfaces")
|
|
730
|
+
import_summary.append(f"{len(lan_interfaces)} LAN interfaces")
|
|
885
731
|
import_summary.append(f"{len(network_ranges)} network ranges")
|
|
886
|
-
elif
|
|
732
|
+
elif sites_only:
|
|
887
733
|
import_summary.append(f"{len(sites)} sites only")
|
|
888
|
-
elif
|
|
734
|
+
elif wan_only:
|
|
735
|
+
import_summary.append(f"{len(wan_interfaces)} WAN interfaces only")
|
|
736
|
+
elif lan_only:
|
|
889
737
|
import_summary.append(f"{len(lan_interfaces)} LAN interfaces only")
|
|
890
|
-
elif
|
|
738
|
+
elif ranges_only:
|
|
891
739
|
import_summary.append(f"{len(network_ranges)} network ranges only")
|
|
892
740
|
|
|
893
741
|
print(f"\n Ready to import {', '.join(import_summary)}.")
|
|
@@ -903,8 +751,8 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
903
751
|
total_successful = 0
|
|
904
752
|
total_failed = 0
|
|
905
753
|
|
|
906
|
-
# Import sites first (if
|
|
907
|
-
if not
|
|
754
|
+
# Import sites first (if selected)
|
|
755
|
+
if (sites_only or not (wan_only or lan_only or ranges_only)) and sites:
|
|
908
756
|
successful, failed = import_socket_sites(sites, module_name=args.module_name,
|
|
909
757
|
verbose=args.verbose, batch_size=args.batch_size,
|
|
910
758
|
delay_between_batches=args.delay,
|
|
@@ -912,8 +760,8 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
912
760
|
total_successful += successful
|
|
913
761
|
total_failed += failed
|
|
914
762
|
|
|
915
|
-
# Import WAN interfaces (if
|
|
916
|
-
if not
|
|
763
|
+
# Import WAN interfaces (if selected)
|
|
764
|
+
if (wan_only or (not sites_only and not lan_only and not ranges_only)) and wan_interfaces:
|
|
917
765
|
successful, failed = import_wan_interfaces(wan_interfaces, module_name=args.module_name,
|
|
918
766
|
verbose=args.verbose, batch_size=args.batch_size,
|
|
919
767
|
delay_between_batches=args.delay,
|
|
@@ -921,8 +769,8 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
921
769
|
total_successful += successful
|
|
922
770
|
total_failed += failed
|
|
923
771
|
|
|
924
|
-
# Import LAN interfaces (if
|
|
925
|
-
if not
|
|
772
|
+
# Import LAN interfaces (if selected)
|
|
773
|
+
if (lan_only or (not sites_only and not wan_only and not ranges_only)) and lan_interfaces:
|
|
926
774
|
successful, failed = import_lan_interfaces(lan_interfaces, module_name=args.module_name,
|
|
927
775
|
verbose=args.verbose, batch_size=args.batch_size,
|
|
928
776
|
delay_between_batches=args.delay,
|
|
@@ -930,9 +778,9 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
930
778
|
total_successful += successful
|
|
931
779
|
total_failed += failed
|
|
932
780
|
|
|
933
|
-
# Import network ranges (if
|
|
934
|
-
if not
|
|
935
|
-
successful, failed = import_network_ranges(network_ranges, module_name=args.module_name,
|
|
781
|
+
# Import network ranges (if selected)
|
|
782
|
+
if (ranges_only or (not sites_only and not wan_only and not lan_only)) and network_ranges:
|
|
783
|
+
successful, failed = import_network_ranges(network_ranges, lan_interfaces, module_name=args.module_name,
|
|
936
784
|
verbose=args.verbose, batch_size=args.batch_size,
|
|
937
785
|
delay_between_batches=args.delay,
|
|
938
786
|
auto_approve=getattr(args, 'auto_approve', False))
|
|
@@ -962,3 +810,572 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
962
810
|
except Exception as e:
|
|
963
811
|
print(f"ERROR: {str(e)}")
|
|
964
812
|
return [{"success": False, "error": str(e)}]
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def load_csv_data(csv_file, sites_config_dir=None):
|
|
816
|
+
"""
|
|
817
|
+
Load socket sites data from CSV files
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
csv_file: Main sites CSV file
|
|
821
|
+
sites_config_dir: Directory containing network ranges CSV files
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
List of sites in JSON format compatible with existing functions
|
|
825
|
+
"""
|
|
826
|
+
try:
|
|
827
|
+
# Load main sites CSV and group by site
|
|
828
|
+
sites_dict = {}
|
|
829
|
+
with open(csv_file, 'r', newline='', encoding='utf-8') as f:
|
|
830
|
+
reader = csv.DictReader(f)
|
|
831
|
+
for row in reader:
|
|
832
|
+
if not row['site_name'].strip():
|
|
833
|
+
continue
|
|
834
|
+
|
|
835
|
+
site_name = row['site_name']
|
|
836
|
+
site_id = row['site_id'].strip()
|
|
837
|
+
|
|
838
|
+
# If this is the first row for this site (has site_id), create the site entry
|
|
839
|
+
if site_id and site_name not in sites_dict:
|
|
840
|
+
sites_dict[site_name] = {
|
|
841
|
+
'id': site_id,
|
|
842
|
+
'name': site_name,
|
|
843
|
+
'description': row['site_description'],
|
|
844
|
+
'type': row['site_type'],
|
|
845
|
+
'connection_type': row['connection_type'],
|
|
846
|
+
'site_location': {
|
|
847
|
+
'countryCode': row['site_location_country_code'],
|
|
848
|
+
'stateCode': row['site_location_state_code'],
|
|
849
|
+
'city': row['site_location_city'],
|
|
850
|
+
'address': row['site_location_address'],
|
|
851
|
+
'timezone': row['site_location_timezone']
|
|
852
|
+
},
|
|
853
|
+
'native_range': {
|
|
854
|
+
'interface_id': row.get('native_range_interface_id', ''), # May not be in parent CSV
|
|
855
|
+
'interface_name': row['native_range_interface_name'],
|
|
856
|
+
'subnet': row['native_range_subnet'],
|
|
857
|
+
'index': row['native_range_interface_index'],
|
|
858
|
+
'range_name': row.get('native_range_name', ''),
|
|
859
|
+
'range_id': row.get('native_range_id', ''),
|
|
860
|
+
'vlan': row.get('native_range_vlan', None),
|
|
861
|
+
'mdns_reflector': row['native_range_mdns_reflector'].upper() == 'TRUE' if row['native_range_mdns_reflector'] else False,
|
|
862
|
+
'gateway': row['native_range_gateway'] or None,
|
|
863
|
+
'range_type': row['native_range_type'],
|
|
864
|
+
'translated_subnet': row['native_range_translated_subnet'] or None,
|
|
865
|
+
'local_ip': row['native_range_local_ip'],
|
|
866
|
+
'dhcp_settings': {
|
|
867
|
+
'dhcp_type': row['native_range_dhcp_type'] or 'DHCP_DISABLED',
|
|
868
|
+
'ip_range': row['native_range_dhcp_ip_range'] or None,
|
|
869
|
+
'relay_group_id': row['native_range_dhcp_relay_group_id'] or None,
|
|
870
|
+
'relay_group_name': row['native_range_dhcp_relay_group_name'] or None,
|
|
871
|
+
'dhcp_microsegmentation': row['native_range_dhcp_microsegmentation'].upper() == 'TRUE' if row['native_range_dhcp_microsegmentation'] else False
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
'wan_interfaces': [],
|
|
875
|
+
'lan_interfaces': []
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
# Add default LAN interface from parent CSV native range data
|
|
879
|
+
# This ensures every site has its default LAN interface for import
|
|
880
|
+
# Note: interface_id may be provided later from site-specific CSV files
|
|
881
|
+
if row['native_range_interface_index']:
|
|
882
|
+
default_lan_interface = {
|
|
883
|
+
'id': row.get('native_range_interface_id', ''), # May be empty, will be filled from site CSV
|
|
884
|
+
'name': row['native_range_interface_name'],
|
|
885
|
+
'index': row['native_range_interface_index'],
|
|
886
|
+
'dest_type': 'LAN',
|
|
887
|
+
'default_lan': True,
|
|
888
|
+
'network_ranges': [] # Default interfaces typically don't have additional ranges
|
|
889
|
+
}
|
|
890
|
+
sites_dict[site_name]['lan_interfaces'].append(default_lan_interface)
|
|
891
|
+
|
|
892
|
+
# Add WAN interface from current row if WAN interface data exists
|
|
893
|
+
wan_id = row.get('wan_interface_id', '')
|
|
894
|
+
if wan_id.strip() and site_name in sites_dict:
|
|
895
|
+
wan_interface = {
|
|
896
|
+
'id': wan_id,
|
|
897
|
+
'index': row.get('wan_interface_index', ''),
|
|
898
|
+
'name': row.get('wan_interface_name', ''),
|
|
899
|
+
'upstream_bandwidth': int(row['wan_upstream_bw']) if row.get('wan_upstream_bw', '').strip() else 25,
|
|
900
|
+
'downstream_bandwidth': int(row['wan_downstream_bw']) if row.get('wan_downstream_bw', '').strip() else 25,
|
|
901
|
+
'dest_type': 'CATO', # Default, not available in current CSV format
|
|
902
|
+
'role': row.get('wan_role', ''),
|
|
903
|
+
'precedence': row.get('wan_precedence', 'ACTIVE')
|
|
904
|
+
}
|
|
905
|
+
sites_dict[site_name]['wan_interfaces'].append(wan_interface)
|
|
906
|
+
|
|
907
|
+
# Convert sites dictionary to list
|
|
908
|
+
sites = list(sites_dict.values())
|
|
909
|
+
|
|
910
|
+
# Load network ranges CSV files if sites_config_dir is provided
|
|
911
|
+
if sites_config_dir and os.path.exists(sites_config_dir):
|
|
912
|
+
# Get list of all CSV files in the directory
|
|
913
|
+
available_files = [f for f in os.listdir(sites_config_dir) if f.endswith('_network_ranges.csv')]
|
|
914
|
+
|
|
915
|
+
for site in sites:
|
|
916
|
+
site_name = site['name']
|
|
917
|
+
ranges_file_found = None
|
|
918
|
+
|
|
919
|
+
# Try different filename patterns to find the matching file
|
|
920
|
+
potential_names = [
|
|
921
|
+
site_name, # Exact name
|
|
922
|
+
site_name.replace(' ', '-'), # Spaces to dashes
|
|
923
|
+
site_name.replace(' ', '_'), # Spaces to underscores
|
|
924
|
+
site_name.replace('-', '_'), # Dashes to underscores
|
|
925
|
+
site_name.replace('/', '-'), # Slashes to dashes
|
|
926
|
+
site_name.replace('/', '_'), # Slashes to underscores
|
|
927
|
+
# Additional transformations for special cases
|
|
928
|
+
re.sub(r'[^a-zA-Z0-9_-]', '_', site_name), # Replace all special chars with underscores
|
|
929
|
+
re.sub(r'[^a-zA-Z0-9_-]', '-', site_name), # Replace all special chars with dashes
|
|
930
|
+
re.sub(r'[^a-zA-Z0-9]', '', site_name), # Remove all special chars
|
|
931
|
+
site_name.replace(' ', ''), # Remove all spaces
|
|
932
|
+
]
|
|
933
|
+
|
|
934
|
+
# Look for matching file
|
|
935
|
+
for potential_name in potential_names:
|
|
936
|
+
expected_filename = f"{potential_name}_network_ranges.csv"
|
|
937
|
+
if expected_filename in available_files:
|
|
938
|
+
ranges_file_found = os.path.join(sites_config_dir, expected_filename)
|
|
939
|
+
break
|
|
940
|
+
|
|
941
|
+
if ranges_file_found:
|
|
942
|
+
load_site_network_ranges_csv(site, ranges_file_found)
|
|
943
|
+
else:
|
|
944
|
+
print(f"Warning: Network ranges file not found for site '{site_name}'. Tried: {[f'{name}_network_ranges.csv' for name in potential_names]}")
|
|
945
|
+
print(f" Available files: {available_files}")
|
|
946
|
+
|
|
947
|
+
return sites
|
|
948
|
+
|
|
949
|
+
except FileNotFoundError:
|
|
950
|
+
print(f"Error: CSV file '{csv_file}' not found")
|
|
951
|
+
sys.exit(1)
|
|
952
|
+
except Exception as e:
|
|
953
|
+
print(f"Error loading CSV data from '{csv_file}': {e}")
|
|
954
|
+
sys.exit(1)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def load_site_network_ranges_csv(site, ranges_csv_file):
|
|
958
|
+
"""
|
|
959
|
+
Load network ranges for a site from CSV file and add to site data structure
|
|
960
|
+
New CSV structure:
|
|
961
|
+
- Rows with lan_interface_id create/define LAN interfaces
|
|
962
|
+
- Rows with network_range_id add network ranges to the current interface
|
|
963
|
+
- is_native_range indicates if a network range is native for that interface
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
site: Site dictionary to add ranges to
|
|
967
|
+
ranges_csv_file: Path to network ranges CSV file
|
|
968
|
+
"""
|
|
969
|
+
try:
|
|
970
|
+
# Load CLI settings to get default interface mapping
|
|
971
|
+
from ....Utils.cliutils import load_cli_settings
|
|
972
|
+
settings = load_cli_settings()
|
|
973
|
+
# Note: load_cli_settings() now returns embedded defaults if file cannot be loaded
|
|
974
|
+
if not settings.get("default_socket_interface_map"):
|
|
975
|
+
print(f"Warning: No default socket interface mapping found for site {site['name']}")
|
|
976
|
+
|
|
977
|
+
with open(ranges_csv_file, 'r', newline='', encoding='utf-8') as f:
|
|
978
|
+
reader = csv.DictReader(f)
|
|
979
|
+
|
|
980
|
+
# Store interfaces by lan_interface_index for processing
|
|
981
|
+
interfaces = {}
|
|
982
|
+
current_interface_index = None
|
|
983
|
+
current_interface_data = None
|
|
984
|
+
|
|
985
|
+
for row in reader:
|
|
986
|
+
# Clean up row data (remove carriage returns)
|
|
987
|
+
cleaned_row = {k: v.strip() if isinstance(v, str) else v for k, v in row.items()}
|
|
988
|
+
row = cleaned_row
|
|
989
|
+
|
|
990
|
+
# Check if this row defines a LAN interface (has lan_interface_id)
|
|
991
|
+
has_lan_interface_id = bool(row.get('lan_interface_id', '').strip())
|
|
992
|
+
lan_interface_index = row.get('lan_interface_index', '').strip()
|
|
993
|
+
|
|
994
|
+
# Check if this is a default LAN interface (no interface ID but has index matching default)
|
|
995
|
+
is_default_interface = False
|
|
996
|
+
if not has_lan_interface_id and lan_interface_index:
|
|
997
|
+
connection_type = site.get('connection_type', '')
|
|
998
|
+
default_interface_index = settings.get("default_socket_interface_map", {}).get(connection_type)
|
|
999
|
+
if default_interface_index and lan_interface_index == default_interface_index:
|
|
1000
|
+
is_default_interface = True
|
|
1001
|
+
|
|
1002
|
+
# If this row has a LAN interface ID, create/update the interface
|
|
1003
|
+
# OR if this is a default interface, get details from parent CSV
|
|
1004
|
+
if (has_lan_interface_id or is_default_interface) and lan_interface_index:
|
|
1005
|
+
|
|
1006
|
+
# Create or get the interface data
|
|
1007
|
+
if lan_interface_index not in interfaces:
|
|
1008
|
+
if is_default_interface:
|
|
1009
|
+
# For default interfaces, get details from parent CSV native_range
|
|
1010
|
+
native_range = site.get('native_range', {})
|
|
1011
|
+
interfaces[lan_interface_index] = {
|
|
1012
|
+
'id': native_range.get('interface_id', ''),
|
|
1013
|
+
'name': native_range.get('interface_name', ''),
|
|
1014
|
+
'index': lan_interface_index,
|
|
1015
|
+
'dest_type': 'LAN', # Default for default interfaces
|
|
1016
|
+
'default_lan': True, # Mark as default interface
|
|
1017
|
+
'network_ranges': []
|
|
1018
|
+
}
|
|
1019
|
+
else:
|
|
1020
|
+
# For regular interfaces, get details from CSV row
|
|
1021
|
+
interfaces[lan_interface_index] = {
|
|
1022
|
+
'id': row['lan_interface_id'],
|
|
1023
|
+
'name': row.get('lan_interface_name', ''),
|
|
1024
|
+
'index': lan_interface_index,
|
|
1025
|
+
'dest_type': row.get('lan_interface_dest_type', 'LAN'),
|
|
1026
|
+
'default_lan': False, # Will be determined by presence in native_range
|
|
1027
|
+
'network_ranges': []
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
current_interface_index = lan_interface_index
|
|
1031
|
+
current_interface_data = interfaces[lan_interface_index]
|
|
1032
|
+
|
|
1033
|
+
# If no new interface but we have a lan_interface_index, use existing interface
|
|
1034
|
+
elif lan_interface_index and lan_interface_index in interfaces:
|
|
1035
|
+
# This row continues with the same interface (no new lan_interface_id but same index)
|
|
1036
|
+
current_interface_index = lan_interface_index
|
|
1037
|
+
current_interface_data = interfaces[lan_interface_index]
|
|
1038
|
+
|
|
1039
|
+
# If this CSV row doesn't have a lan_interface_id but has network ranges,
|
|
1040
|
+
# mark the interface as virtual so network ranges get processed
|
|
1041
|
+
if not has_lan_interface_id and row.get('network_range_id', '').strip():
|
|
1042
|
+
current_interface_data['virtual_interface'] = True
|
|
1043
|
+
# If we have a lan_interface_index but no interface entry, create a virtual interface for processing network ranges
|
|
1044
|
+
elif lan_interface_index and row.get('network_range_id', '').strip():
|
|
1045
|
+
# Create a virtual interface entry for network range processing
|
|
1046
|
+
# This interface won't create a LAN interface resource, but allows network ranges to be processed
|
|
1047
|
+
if lan_interface_index not in interfaces:
|
|
1048
|
+
interfaces[lan_interface_index] = {
|
|
1049
|
+
'id': None, # No interface resource will be created
|
|
1050
|
+
'name': f"Virtual-{lan_interface_index}",
|
|
1051
|
+
'index': lan_interface_index,
|
|
1052
|
+
'dest_type': 'LAN',
|
|
1053
|
+
'default_lan': False,
|
|
1054
|
+
'network_ranges': [],
|
|
1055
|
+
'virtual_interface': True # Mark as virtual
|
|
1056
|
+
}
|
|
1057
|
+
current_interface_index = lan_interface_index
|
|
1058
|
+
current_interface_data = interfaces[lan_interface_index]
|
|
1059
|
+
|
|
1060
|
+
# Also mark this interface as virtual if it wasn't created with an interface ID
|
|
1061
|
+
if not current_interface_data.get('id'):
|
|
1062
|
+
current_interface_data['virtual_interface'] = True
|
|
1063
|
+
|
|
1064
|
+
# Process network range data if present and we have a current interface
|
|
1065
|
+
if current_interface_data and row.get('network_range_id', '').strip():
|
|
1066
|
+
|
|
1067
|
+
network_range = {
|
|
1068
|
+
'id': row['network_range_id'],
|
|
1069
|
+
'name': row.get('network_range_name', ''),
|
|
1070
|
+
'subnet': row.get('subnet', ''),
|
|
1071
|
+
'vlan': int(row['vlan']) if row.get('vlan', '').strip() else None,
|
|
1072
|
+
'mdns_reflector': row.get('mdns_reflector', '').upper() == 'TRUE',
|
|
1073
|
+
'gateway': row.get('gateway') or None,
|
|
1074
|
+
'range_type': row.get('range_type', ''),
|
|
1075
|
+
'translated_subnet': row.get('translated_subnet') or None,
|
|
1076
|
+
'local_ip': row.get('local_ip', ''),
|
|
1077
|
+
'native_range': row.get('is_native_range', '').upper() == 'TRUE', # update this to support json native_range=true
|
|
1078
|
+
'dhcp_settings': {
|
|
1079
|
+
'dhcp_type': row.get('dhcp_type', '') or 'DHCP_DISABLED',
|
|
1080
|
+
'ip_range': row.get('dhcp_ip_range') or None,
|
|
1081
|
+
'relay_group_id': row.get('dhcp_relay_group_id') or None,
|
|
1082
|
+
'relay_group_name': row.get('dhcp_relay_group_name') or None,
|
|
1083
|
+
'dhcp_microsegmentation': row.get('dhcp_microsegmentation', '').upper() == 'TRUE'
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
# Add network range to current interface
|
|
1088
|
+
current_interface_data['network_ranges'].append(network_range)
|
|
1089
|
+
|
|
1090
|
+
# Check if this interface should be marked as default_lan
|
|
1091
|
+
# by checking if this is marked as a native range
|
|
1092
|
+
is_native_range = row.get('is_native_range', '').upper() == 'TRUE'
|
|
1093
|
+
if is_native_range:
|
|
1094
|
+
native_range = site.get('native_range', {})
|
|
1095
|
+
interface_matches_native = (
|
|
1096
|
+
current_interface_data['index'] == native_range.get('index') or
|
|
1097
|
+
current_interface_data['name'] == native_range.get('interface_name')
|
|
1098
|
+
)
|
|
1099
|
+
if interface_matches_native:
|
|
1100
|
+
current_interface_data['default_lan'] = True
|
|
1101
|
+
# IMPORTANT: Do not add this network range to the interface's network_ranges
|
|
1102
|
+
# because it's the site's native range and will be handled separately
|
|
1103
|
+
# Remove it from the network_ranges list - it was just added above
|
|
1104
|
+
current_interface_data['network_ranges'].pop()
|
|
1105
|
+
# Skip processing this network range further
|
|
1106
|
+
continue
|
|
1107
|
+
|
|
1108
|
+
# Add interfaces to site, but first merge with any existing default interface
|
|
1109
|
+
existing_interfaces = site.get('lan_interfaces', [])
|
|
1110
|
+
new_interfaces = list(interfaces.values())
|
|
1111
|
+
|
|
1112
|
+
# Check for default LAN interface conflicts and merge
|
|
1113
|
+
final_interfaces = []
|
|
1114
|
+
default_interface_found = False
|
|
1115
|
+
|
|
1116
|
+
# First add existing interfaces, updating any that match new interfaces
|
|
1117
|
+
for existing_interface in existing_interfaces:
|
|
1118
|
+
if existing_interface.get('default_lan', False):
|
|
1119
|
+
# This is the default interface from parent CSV
|
|
1120
|
+
default_interface_found = True
|
|
1121
|
+
existing_index = existing_interface.get('index')
|
|
1122
|
+
|
|
1123
|
+
# Check if we have new data for the same interface
|
|
1124
|
+
matching_new_interface = None
|
|
1125
|
+
for new_interface in new_interfaces:
|
|
1126
|
+
if (new_interface.get('index') == existing_index or
|
|
1127
|
+
new_interface.get('id') == existing_interface.get('id')):
|
|
1128
|
+
matching_new_interface = new_interface
|
|
1129
|
+
break
|
|
1130
|
+
|
|
1131
|
+
if matching_new_interface:
|
|
1132
|
+
# Merge the interfaces - keep default_lan=True from existing, but use any additional network ranges from new
|
|
1133
|
+
merged_interface = existing_interface.copy()
|
|
1134
|
+
merged_interface['network_ranges'] = matching_new_interface.get('network_ranges', [])
|
|
1135
|
+
# Preserve the virtual_interface flag from the new interface if present
|
|
1136
|
+
if matching_new_interface.get('virtual_interface'):
|
|
1137
|
+
merged_interface['virtual_interface'] = True
|
|
1138
|
+
final_interfaces.append(merged_interface)
|
|
1139
|
+
# Remove the matching interface from new_interfaces to avoid duplication
|
|
1140
|
+
new_interfaces.remove(matching_new_interface)
|
|
1141
|
+
else:
|
|
1142
|
+
# No new data for default interface, keep as-is
|
|
1143
|
+
final_interfaces.append(existing_interface)
|
|
1144
|
+
else:
|
|
1145
|
+
# Non-default existing interface, keep as-is
|
|
1146
|
+
final_interfaces.append(existing_interface)
|
|
1147
|
+
|
|
1148
|
+
# Add any remaining new interfaces that didn't match existing ones
|
|
1149
|
+
final_interfaces.extend(new_interfaces)
|
|
1150
|
+
|
|
1151
|
+
# Update site's lan_interfaces
|
|
1152
|
+
site['lan_interfaces'] = final_interfaces
|
|
1153
|
+
|
|
1154
|
+
except FileNotFoundError:
|
|
1155
|
+
print(f"Warning: Network ranges file '{ranges_csv_file}' not found for site {site['name']}")
|
|
1156
|
+
except Exception as e:
|
|
1157
|
+
print(f"Error loading network ranges from '{ranges_csv_file}': {e}")
|
|
1158
|
+
import traceback
|
|
1159
|
+
traceback.print_exc()
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def import_socket_sites_from_csv(args, configuration):
|
|
1163
|
+
"""
|
|
1164
|
+
Main function to orchestrate the socket sites import process from CSV files
|
|
1165
|
+
"""
|
|
1166
|
+
try:
|
|
1167
|
+
print(" Terraform Import Tool - Cato Socket Sites from CSV")
|
|
1168
|
+
print("=" * 70)
|
|
1169
|
+
|
|
1170
|
+
# Determine sites config directory
|
|
1171
|
+
sites_config_dir = None
|
|
1172
|
+
if hasattr(args, 'sites_config_dir') and args.sites_config_dir:
|
|
1173
|
+
sites_config_dir = args.sites_config_dir
|
|
1174
|
+
else:
|
|
1175
|
+
# Try to find sites_config directory relative to CSV file
|
|
1176
|
+
csv_dir = os.path.dirname(os.path.abspath(args.csv_file))
|
|
1177
|
+
potential_config_dir = os.path.join(csv_dir, 'sites_config')
|
|
1178
|
+
if os.path.exists(potential_config_dir):
|
|
1179
|
+
sites_config_dir = potential_config_dir
|
|
1180
|
+
|
|
1181
|
+
# Load data from CSV
|
|
1182
|
+
print(f" Loading data from {args.csv_file}...")
|
|
1183
|
+
if sites_config_dir:
|
|
1184
|
+
print(f" Loading network ranges from {sites_config_dir}...")
|
|
1185
|
+
|
|
1186
|
+
sites_data = load_csv_data(args.csv_file, sites_config_dir)
|
|
1187
|
+
|
|
1188
|
+
# Extract sites, WAN interfaces, LAN interfaces, and network ranges using existing function
|
|
1189
|
+
sites, wan_interfaces, lan_interfaces, network_ranges = extract_socket_sites_data(sites_data)
|
|
1190
|
+
|
|
1191
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
1192
|
+
print(f"\nExtracted data summary:")
|
|
1193
|
+
print(f" Sites: {len(sites)}")
|
|
1194
|
+
print(f" WAN Interfaces: {len(wan_interfaces)}")
|
|
1195
|
+
print(f" LAN Interfaces: {len(lan_interfaces)}")
|
|
1196
|
+
print(f" Network Ranges: {len(network_ranges)}")
|
|
1197
|
+
|
|
1198
|
+
print(f" Found {len(sites)} sites")
|
|
1199
|
+
print(f" Found {len(wan_interfaces)} WAN interfaces")
|
|
1200
|
+
print(f" Found {len(lan_interfaces)} LAN interfaces")
|
|
1201
|
+
print(f" Found {len(network_ranges)} network ranges")
|
|
1202
|
+
|
|
1203
|
+
if not sites and not wan_interfaces and not network_ranges:
|
|
1204
|
+
print(" No sites, interfaces, or network ranges found. Exiting.")
|
|
1205
|
+
return [{"success": False, "error": "No data found to import"}]
|
|
1206
|
+
|
|
1207
|
+
# Add module. prefix if not present
|
|
1208
|
+
module_name = args.module_name
|
|
1209
|
+
if not module_name.startswith('module.'):
|
|
1210
|
+
module_name = f'module.{module_name}'
|
|
1211
|
+
|
|
1212
|
+
# Generate Terraform configuration files if requested
|
|
1213
|
+
if hasattr(args, 'generate_only') and args.generate_only:
|
|
1214
|
+
print("\nGenerating Terraform configuration files...")
|
|
1215
|
+
output_dir = generate_terraform_import_files(sites, output_dir=getattr(args, 'output_dir', './imported_sites'))
|
|
1216
|
+
print(f"\nTerraform configuration files generated successfully in {output_dir}")
|
|
1217
|
+
print("\nNext steps:")
|
|
1218
|
+
print(f" 1. Copy the generated files to your Terraform project directory")
|
|
1219
|
+
print(f" 2. Run 'terraform init' to initialize")
|
|
1220
|
+
print(f" 3. Run 'terraform plan -generate-config-out=generated.tf' to generate configuration")
|
|
1221
|
+
print(f" 4. Run 'terraform apply' to import the resources")
|
|
1222
|
+
|
|
1223
|
+
return [{
|
|
1224
|
+
"success": True,
|
|
1225
|
+
"total_generated": len(sites),
|
|
1226
|
+
"output_dir": output_dir
|
|
1227
|
+
}]
|
|
1228
|
+
|
|
1229
|
+
# Validate Terraform environment before proceeding
|
|
1230
|
+
validate_terraform_environment(module_name, verbose=args.verbose)
|
|
1231
|
+
|
|
1232
|
+
# Ask for confirmation (unless auto-approved)
|
|
1233
|
+
# Determine which categories to import based on flags
|
|
1234
|
+
sites_only = getattr(args, 'sites_only', False)
|
|
1235
|
+
wan_only = getattr(args, 'wan_interfaces_only', False)
|
|
1236
|
+
lan_only = getattr(args, 'lan_interfaces_only', False)
|
|
1237
|
+
ranges_only = getattr(args, 'network_ranges_only', False)
|
|
1238
|
+
|
|
1239
|
+
import_summary = []
|
|
1240
|
+
if not (sites_only or wan_only or lan_only or ranges_only):
|
|
1241
|
+
import_summary.append(f"{len(sites)} sites")
|
|
1242
|
+
import_summary.append(f"{len(wan_interfaces)} WAN interfaces")
|
|
1243
|
+
import_summary.append(f"{len(lan_interfaces)} LAN interfaces")
|
|
1244
|
+
import_summary.append(f"{len(network_ranges)} network ranges")
|
|
1245
|
+
elif sites_only:
|
|
1246
|
+
import_summary.append(f"{len(sites)} sites only")
|
|
1247
|
+
elif wan_only:
|
|
1248
|
+
import_summary.append(f"{len(wan_interfaces)} WAN interfaces only")
|
|
1249
|
+
elif lan_only:
|
|
1250
|
+
import_summary.append(f"{len(lan_interfaces)} LAN interfaces only")
|
|
1251
|
+
elif ranges_only:
|
|
1252
|
+
import_summary.append(f"{len(network_ranges)} network ranges only")
|
|
1253
|
+
|
|
1254
|
+
print(f"\n Ready to import {', '.join(import_summary)}.")
|
|
1255
|
+
|
|
1256
|
+
if hasattr(args, 'auto_approve') and args.auto_approve:
|
|
1257
|
+
print("\nAuto-approve enabled, proceeding with import...")
|
|
1258
|
+
else:
|
|
1259
|
+
confirm = input(f"\nProceed with import? (y/n): ").lower()
|
|
1260
|
+
if confirm != 'y':
|
|
1261
|
+
print("Import cancelled.")
|
|
1262
|
+
return [{"success": False, "error": "Import cancelled by user"}]
|
|
1263
|
+
|
|
1264
|
+
total_successful = 0
|
|
1265
|
+
total_failed = 0
|
|
1266
|
+
|
|
1267
|
+
# Import sites first (if selected)
|
|
1268
|
+
if (sites_only or not (wan_only or lan_only or ranges_only)) and sites:
|
|
1269
|
+
successful, failed = import_socket_sites(sites, module_name=args.module_name,
|
|
1270
|
+
verbose=args.verbose, batch_size=getattr(args, 'batch_size', 10),
|
|
1271
|
+
delay_between_batches=getattr(args, 'delay', 2),
|
|
1272
|
+
auto_approve=getattr(args, 'auto_approve', False))
|
|
1273
|
+
total_successful += successful
|
|
1274
|
+
total_failed += failed
|
|
1275
|
+
|
|
1276
|
+
# Import WAN interfaces (if selected)
|
|
1277
|
+
if (wan_only or (not sites_only and not lan_only and not ranges_only)) and wan_interfaces:
|
|
1278
|
+
successful, failed = import_wan_interfaces(wan_interfaces, module_name=args.module_name,
|
|
1279
|
+
verbose=args.verbose, batch_size=getattr(args, 'batch_size', 10),
|
|
1280
|
+
delay_between_batches=getattr(args, 'delay', 2),
|
|
1281
|
+
auto_approve=getattr(args, 'auto_approve', False))
|
|
1282
|
+
total_successful += successful
|
|
1283
|
+
total_failed += failed
|
|
1284
|
+
|
|
1285
|
+
# Import LAN interfaces (if selected)
|
|
1286
|
+
if (lan_only or (not sites_only and not wan_only and not ranges_only)) and lan_interfaces:
|
|
1287
|
+
successful, failed = import_lan_interfaces(lan_interfaces, module_name=args.module_name,
|
|
1288
|
+
verbose=args.verbose, batch_size=getattr(args, 'batch_size', 10),
|
|
1289
|
+
delay_between_batches=getattr(args, 'delay', 2),
|
|
1290
|
+
auto_approve=getattr(args, 'auto_approve', False))
|
|
1291
|
+
total_successful += successful
|
|
1292
|
+
total_failed += failed
|
|
1293
|
+
|
|
1294
|
+
# Import network ranges (if selected)
|
|
1295
|
+
if (ranges_only or (not sites_only and not wan_only and not lan_only)) and network_ranges:
|
|
1296
|
+
successful, failed = import_network_ranges(network_ranges, lan_interfaces, module_name=args.module_name,
|
|
1297
|
+
verbose=args.verbose, batch_size=getattr(args, 'batch_size', 10),
|
|
1298
|
+
delay_between_batches=getattr(args, 'delay', 2),
|
|
1299
|
+
auto_approve=getattr(args, 'auto_approve', False))
|
|
1300
|
+
total_successful += successful
|
|
1301
|
+
total_failed += failed
|
|
1302
|
+
|
|
1303
|
+
# Final summary
|
|
1304
|
+
print("\n" + "=" * 70)
|
|
1305
|
+
print(" FINAL IMPORT SUMMARY")
|
|
1306
|
+
print("=" * 70)
|
|
1307
|
+
print(f" Total successful imports: {total_successful}")
|
|
1308
|
+
print(f" Total failed imports: {total_failed}")
|
|
1309
|
+
print(f" Overall success rate: {(total_successful / (total_successful + total_failed) * 100):.1f}%" if (total_successful + total_failed) > 0 else "N/A")
|
|
1310
|
+
print("\n Import process completed!")
|
|
1311
|
+
|
|
1312
|
+
return [{
|
|
1313
|
+
"success": True,
|
|
1314
|
+
"total_successful": total_successful,
|
|
1315
|
+
"total_failed": total_failed,
|
|
1316
|
+
"module_name": args.module_name
|
|
1317
|
+
}]
|
|
1318
|
+
|
|
1319
|
+
except KeyboardInterrupt:
|
|
1320
|
+
print("\nImport process cancelled by user (Ctrl+C).")
|
|
1321
|
+
print("Partial imports may have been completed.")
|
|
1322
|
+
return [{"success": False, "error": "Import cancelled by user"}]
|
|
1323
|
+
except Exception as e:
|
|
1324
|
+
print(f"ERROR: {str(e)}")
|
|
1325
|
+
return [{"success": False, "error": str(e)}]
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def convert_csv_to_json(args, configuration):
|
|
1329
|
+
"""
|
|
1330
|
+
Convert CSV data to JSON format compatible with existing import tools
|
|
1331
|
+
"""
|
|
1332
|
+
try:
|
|
1333
|
+
print(" CSV to JSON Converter - Cato Socket Sites")
|
|
1334
|
+
print("=" * 50)
|
|
1335
|
+
|
|
1336
|
+
# Determine sites config directory
|
|
1337
|
+
sites_config_dir = None
|
|
1338
|
+
if hasattr(args, 'sites_config_dir') and args.sites_config_dir:
|
|
1339
|
+
sites_config_dir = args.sites_config_dir
|
|
1340
|
+
else:
|
|
1341
|
+
# Try to find sites_config directory relative to CSV file
|
|
1342
|
+
csv_dir = os.path.dirname(os.path.abspath(args.csv_file))
|
|
1343
|
+
potential_config_dir = os.path.join(csv_dir, 'sites_config')
|
|
1344
|
+
if os.path.exists(potential_config_dir):
|
|
1345
|
+
sites_config_dir = potential_config_dir
|
|
1346
|
+
|
|
1347
|
+
# Load data from CSV
|
|
1348
|
+
print(f" Loading data from {args.csv_file}...")
|
|
1349
|
+
if sites_config_dir:
|
|
1350
|
+
print(f" Loading network ranges from {sites_config_dir}...")
|
|
1351
|
+
|
|
1352
|
+
sites_data = load_csv_data(args.csv_file, sites_config_dir)
|
|
1353
|
+
|
|
1354
|
+
# Create JSON structure
|
|
1355
|
+
json_data = {"sites": sites_data}
|
|
1356
|
+
|
|
1357
|
+
# Determine output filename
|
|
1358
|
+
if hasattr(args, 'output_file') and args.output_file:
|
|
1359
|
+
output_file = args.output_file
|
|
1360
|
+
else:
|
|
1361
|
+
# Generate output filename based on input CSV
|
|
1362
|
+
csv_base = os.path.splitext(os.path.basename(args.csv_file))[0]
|
|
1363
|
+
output_file = f"{csv_base}_converted.json"
|
|
1364
|
+
|
|
1365
|
+
# Write JSON file
|
|
1366
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
1367
|
+
json.dump(json_data, f, indent=2)
|
|
1368
|
+
|
|
1369
|
+
print(f" Converted {len(sites_data)} sites to JSON format")
|
|
1370
|
+
print(f" Output written to: {output_file}")
|
|
1371
|
+
|
|
1372
|
+
return [{
|
|
1373
|
+
"success": True,
|
|
1374
|
+
"input_file": args.csv_file,
|
|
1375
|
+
"output_file": output_file,
|
|
1376
|
+
"sites_count": len(sites_data)
|
|
1377
|
+
}]
|
|
1378
|
+
|
|
1379
|
+
except Exception as e:
|
|
1380
|
+
print(f"ERROR: {str(e)}")
|
|
1381
|
+
return [{"success": False, "error": str(e)}]
|