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.

Files changed (107) hide show
  1. catocli/Utils/clidriver.py +20 -9
  2. catocli/Utils/cliutils.py +45 -17
  3. catocli/Utils/csv_formatter.py +652 -0
  4. catocli/__init__.py +2 -2
  5. catocli/clisettings.json +35 -0
  6. catocli/parsers/custom/export_rules/__init__.py +0 -4
  7. catocli/parsers/custom/export_sites/__init__.py +17 -5
  8. catocli/parsers/custom/export_sites/export_sites.py +826 -53
  9. catocli/parsers/custom/import_sites_to_tf/__init__.py +44 -16
  10. catocli/parsers/custom/import_sites_to_tf/import_sites_to_tf.py +859 -442
  11. catocli/parsers/customParserApiClient.py +444 -38
  12. catocli/parsers/custom_private/__init__.py +18 -0
  13. catocli/parsers/mutation_accountManagement/__init__.py +21 -0
  14. catocli/parsers/mutation_accountManagement_disableAccount/README.md +15 -0
  15. catocli/parsers/mutation_admin/__init__.py +12 -0
  16. catocli/parsers/mutation_container/__init__.py +18 -0
  17. catocli/parsers/mutation_enterpriseDirectory/__init__.py +8 -0
  18. catocli/parsers/mutation_groups/__init__.py +6 -0
  19. catocli/parsers/mutation_hardware/__init__.py +2 -0
  20. catocli/parsers/mutation_licensing/__init__.py +24 -0
  21. catocli/parsers/mutation_licensing_updateCommercialLicense/README.md +19 -0
  22. catocli/parsers/mutation_policy/__init__.py +861 -483
  23. catocli/parsers/mutation_policy_antiMalwareFileHash_addRule/README.md +20 -0
  24. catocli/parsers/mutation_policy_antiMalwareFileHash_addSection/README.md +20 -0
  25. catocli/parsers/mutation_policy_antiMalwareFileHash_createPolicyRevision/README.md +20 -0
  26. catocli/parsers/mutation_policy_antiMalwareFileHash_discardPolicyRevision/README.md +20 -0
  27. catocli/parsers/mutation_policy_antiMalwareFileHash_moveRule/README.md +20 -0
  28. catocli/parsers/mutation_policy_antiMalwareFileHash_moveSection/README.md +20 -0
  29. catocli/parsers/mutation_policy_antiMalwareFileHash_publishPolicyRevision/README.md +20 -0
  30. catocli/parsers/mutation_policy_antiMalwareFileHash_removeRule/README.md +20 -0
  31. catocli/parsers/mutation_policy_antiMalwareFileHash_removeSection/README.md +20 -0
  32. catocli/parsers/mutation_policy_antiMalwareFileHash_updatePolicy/README.md +20 -0
  33. catocli/parsers/mutation_policy_antiMalwareFileHash_updateRule/README.md +20 -0
  34. catocli/parsers/mutation_policy_antiMalwareFileHash_updateSection/README.md +20 -0
  35. catocli/parsers/mutation_sandbox/__init__.py +4 -0
  36. catocli/parsers/mutation_site/__init__.py +72 -0
  37. catocli/parsers/mutation_sites/__init__.py +72 -0
  38. catocli/parsers/mutation_xdr/__init__.py +6 -0
  39. catocli/parsers/query_accountBySubdomain/__init__.py +2 -0
  40. catocli/parsers/query_accountManagement/__init__.py +2 -0
  41. catocli/parsers/query_accountMetrics/__init__.py +6 -0
  42. catocli/parsers/query_accountRoles/__init__.py +2 -0
  43. catocli/parsers/query_accountSnapshot/__init__.py +2 -0
  44. catocli/parsers/query_admin/__init__.py +2 -0
  45. catocli/parsers/query_admins/__init__.py +2 -0
  46. catocli/parsers/query_appStats/__init__.py +6 -0
  47. catocli/parsers/query_appStatsTimeSeries/README.md +3 -0
  48. catocli/parsers/query_appStatsTimeSeries/__init__.py +6 -0
  49. catocli/parsers/query_auditFeed/__init__.py +2 -0
  50. catocli/parsers/query_catalogs/__init__.py +2 -0
  51. catocli/parsers/query_container/__init__.py +2 -0
  52. catocli/parsers/query_devices/README.md +1 -1
  53. catocli/parsers/query_devices/__init__.py +2 -0
  54. catocli/parsers/query_enterpriseDirectory/__init__.py +2 -0
  55. catocli/parsers/query_entityLookup/__init__.py +2 -0
  56. catocli/parsers/query_events/__init__.py +2 -0
  57. catocli/parsers/query_eventsFeed/README.md +1 -1
  58. catocli/parsers/query_eventsFeed/__init__.py +2 -0
  59. catocli/parsers/query_eventsTimeSeries/__init__.py +2 -0
  60. catocli/parsers/query_groups/__init__.py +6 -0
  61. catocli/parsers/query_hardware/README.md +1 -1
  62. catocli/parsers/query_hardware/__init__.py +2 -0
  63. catocli/parsers/query_hardwareManagement/__init__.py +2 -0
  64. catocli/parsers/query_licensing/__init__.py +2 -0
  65. catocli/parsers/query_policy/__init__.py +85 -48
  66. catocli/parsers/query_policy_antiMalwareFileHash_policy/README.md +19 -0
  67. catocli/parsers/query_popLocations/__init__.py +2 -0
  68. catocli/parsers/query_sandbox/__init__.py +2 -0
  69. catocli/parsers/query_servicePrincipalAdmin/__init__.py +2 -0
  70. catocli/parsers/query_site/__init__.py +33 -0
  71. catocli/parsers/query_siteLocation/__init__.py +2 -0
  72. catocli/parsers/query_site_siteGeneralDetails/README.md +19 -0
  73. catocli/parsers/query_socketPortMetrics/__init__.py +2 -0
  74. catocli/parsers/query_socketPortMetricsTimeSeries/__init__.py +6 -0
  75. catocli/parsers/query_subDomains/__init__.py +2 -0
  76. catocli/parsers/query_xdr/__init__.py +4 -0
  77. catocli/parsers/raw/__init__.py +3 -1
  78. {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/METADATA +1 -1
  79. {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/RECORD +107 -72
  80. models/mutation.accountManagement.disableAccount.json +545 -0
  81. models/mutation.licensing.updateCommercialLicense.json +931 -0
  82. models/mutation.policy.antiMalwareFileHash.addRule.json +2068 -0
  83. models/mutation.policy.antiMalwareFileHash.addSection.json +1350 -0
  84. models/mutation.policy.antiMalwareFileHash.createPolicyRevision.json +1822 -0
  85. models/mutation.policy.antiMalwareFileHash.discardPolicyRevision.json +1758 -0
  86. models/mutation.policy.antiMalwareFileHash.moveRule.json +1552 -0
  87. models/mutation.policy.antiMalwareFileHash.moveSection.json +1251 -0
  88. models/mutation.policy.antiMalwareFileHash.publishPolicyRevision.json +1813 -0
  89. models/mutation.policy.antiMalwareFileHash.removeRule.json +1204 -0
  90. models/mutation.policy.antiMalwareFileHash.removeSection.json +954 -0
  91. models/mutation.policy.antiMalwareFileHash.updatePolicy.json +1834 -0
  92. models/mutation.policy.antiMalwareFileHash.updateRule.json +1757 -0
  93. models/mutation.policy.antiMalwareFileHash.updateSection.json +1105 -0
  94. models/mutation.site.updateSiteGeneralDetails.json +3 -3
  95. models/mutation.sites.updateSiteGeneralDetails.json +3 -3
  96. models/query.devices.json +448 -62
  97. models/query.events.json +216 -0
  98. models/query.eventsFeed.json +48 -0
  99. models/query.eventsTimeSeries.json +144 -0
  100. models/query.hardware.json +224 -0
  101. models/query.policy.antiMalwareFileHash.policy.json +1583 -0
  102. models/query.site.siteGeneralDetails.json +899 -0
  103. schema/catolib.py +51 -4
  104. {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/WHEEL +0 -0
  105. {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/entry_points.txt +0 -0
  106. {catocli-2.1.3.dist-info → catocli-2.1.6.dist-info}/licenses/LICENSE +0 -0
  107. {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
- # Handle dhcp_settings - ensure it's a proper object with required fields
75
- dhcp_settings = site.get('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
- if wan_interface.get('id') and wan_interface.get('name'):
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': wan_interface['id'],
102
- 'name': wan_interface['name'],
103
- 'upstream_bandwidth': wan_interface.get('upstreamBandwidth', 25),
104
- 'downstream_bandwidth': wan_interface.get('downstreamBandwidth', 25),
105
- 'dest_type': wan_interface.get('destType', 'CATO'),
106
- 'role': wan_interface.get('id', 'wan_1'), # Use interface ID as role
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
- lan_interfaces.append({
115
- 'site_id': site['id'],
116
- 'interface_id': lan_interface['id'],
117
- 'name': lan_interface['name'],
118
- 'dest_type': lan_interface.get('destType', 'CATO'),
119
- 'subnet': lan_interface.get('subnet', ''),
120
- 'local_ip': lan_interface.get('local_ip', ''),
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
- for network_range in lan_interface.get('network_ranges', []):
124
- if network_range.get('id') and network_range.get('subnet'):
125
- network_ranges.append({
126
- 'site_id': site['id'],
127
- 'site_name': site['name'],
128
- 'interface_id': interface_id,
129
- 'interface_name': interface_name,
130
- 'network_range_id': network_range['id'],
131
- 'name': network_range.get('rangeName', ''),
132
- 'subnet': network_range['subnet'],
133
- 'vlan_tag': network_range.get('vlanTag', ''),
134
- 'range_type': 'VLAN' if network_range.get('vlanTag') and network_range.get('vlanTag') != '' else 'Native',
135
- 'microsegmentation': network_range.get('microsegmentation', False)
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="wan_interfaces",
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
- # Use correct resource addressing for WAN interfaces
589
- # WAN interfaces use the interface role as the key
590
- wan_role = interface['role']
591
- resource_address = f'{module_name}.module.socket-site["{site_name}"].cato_wan_interface.wan["{wan_role}"]'
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 interface import needs site_id:interface_id format
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 # Already in correct format
345
+ import_id = interface_id
597
346
  else:
598
- import_id = f"{site_id}:{interface_id}" # Need to add site_id
599
- print(f"\n[{i+1}/{total_interfaces}] WAN Interface: {interface_name} on {site_name} (ID: {import_id})")
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
- # Ask user if they want to continue on failure (unless auto-approved)
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="lan_interfaces",
371
+ resource_type="cato_lan_interface", resource_name="interface",
625
372
  batch_size=10, delay_between_batches=2, auto_approve=False):
626
- """Import all WAN interfaces in batches"""
627
- print("\nStarting WAN interface imports...")
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
- # Use correct resource addressing for WAN interfaces
644
- # WAN interfaces use the interface role as the key
645
- lan_role = interface['role']
646
- resource_address = f'{module_name}.module.socket-site["{site_name}"].cato_lan_interface.lan["{interface_index}"]'
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
- # Ask user if they want to continue on failure (unless auto-approved)
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"\nWAN Interface Import Summary: {successful_imports} successful, {failed_imports} failed")
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="network_ranges",
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
- # Use correct resource addressing for network ranges
692
- # Based on terraform plan output, network ranges are structured as:
693
- # module.sites.module.socket-site["site_name"].module.lan_interfaces["interface_id"].module.network_range["subnet"].cato_network_range.no_dhcp[0]
694
- interface_id = network_range['interface_id']
695
- # Convert interface_id to the format used in terraform (e.g., "161570" -> "INT_5")
696
- if interface_id.isdigit():
697
- # This is a numeric interface ID, need to convert to INT_X format
698
- # For now, we'll use the interface_id as is and let terraform handle the mapping
699
- terraform_interface_id = interface_id
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
- terraform_interface_id = interface_id
472
+ # For regular LAN interface ranges, use the formatted interface index
473
+ range_key = f"{formatted_index}-{sanitized_range_name}"
702
474
 
703
- resource_address = f'{module_name}.module.socket-site["{site_name}"].module.lan_interfaces["{terraform_interface_id}"].module.network_range["{network_range_id}"].cato_network_range.no_dhcp[0]'
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} (ID: {network_range_id})")
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
- # Load data
835
- print(f" Loading data from {args.json_file}...")
836
- sites_data = load_json_data(args.json_file)
837
-
838
- # Extract sites, WAN interfaces, and network ranges
839
- sites, wan_interfaces, network_ranges = extract_socket_sites_data(sites_data)
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 args.sites_only and not args.interfaces_only and not args.network_ranges_only:
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 args.sites_only:
732
+ elif sites_only:
887
733
  import_summary.append(f"{len(sites)} sites only")
888
- elif args.lan_interfaces_only:
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 args.network_ranges_only:
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 not skipped)
907
- if not args.interfaces_only and not args.network_ranges_only and sites:
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 not skipped)
916
- if not args.sites_only and not args.network_ranges_only and wan_interfaces:
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 not skipped)
925
- if not args.sites_only and not args.network_ranges_only and lan_interfaces:
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 not skipped)
934
- if not args.sites_only and not args.interfaces_only and network_ranges:
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)}]