Skip to content

Commit 66312cf

Browse files
author
Alex J Lennon
committed
Fix: Pass allow_device_to_device parameter to WgServer constructor
- Fixed load_from_factory classmethod to pass allow_device_to_device parameter to constructor - This ensures the --allow-device-to-device CLI flag works correctly - Code formatting improvements in export_to_google_sheets.py - Import reorganization in tool_handlers.py This fix ensures device-to-device communication works when the flag is enabled.
1 parent 7415904 commit 66312cf

File tree

4 files changed

+82
-69
lines changed

4 files changed

+82
-69
lines changed

export_to_google_sheets.py

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
python export_to_google_sheets.py [--spreadsheet-id SPREADSHEET_ID] [--sheet-name SHEET_NAME]
1515
"""
1616

17+
import argparse
18+
import json
1719
import os
1820
import sys
19-
import json
20-
import argparse
2121
from datetime import datetime
2222
from pathlib import Path
2323

@@ -39,7 +39,7 @@ def get_google_credentials():
3939
"""Get Google Sheets credentials from environment or file."""
4040
# Check environment variable first
4141
creds_path = os.getenv("GOOGLE_SHEETS_CREDENTIALS")
42-
42+
4343
# Fall back to credentials.json in project root
4444
if not creds_path:
4545
creds_path = Path(__file__).parent / "credentials.json"
@@ -48,17 +48,17 @@ def get_google_credentials():
4848
"Google Sheets credentials not found. "
4949
"Set GOOGLE_SHEETS_CREDENTIALS environment variable or place credentials.json in project root."
5050
)
51-
51+
5252
creds_path = Path(creds_path)
5353
if not creds_path.exists():
5454
raise FileNotFoundError(f"Credentials file not found: {creds_path}")
55-
55+
5656
# Load credentials
5757
scope = [
5858
"https://spreadsheets.google.com/feeds",
5959
"https://www.googleapis.com/auth/drive",
6060
]
61-
61+
6262
creds = Credentials.from_service_account_file(str(creds_path), scopes=scope)
6363
return creds
6464

@@ -79,13 +79,13 @@ def create_or_get_spreadsheet(gc, spreadsheet_id=None, title=None):
7979

8080
def export_devices_to_sheet(devices, worksheet, factory_name):
8181
"""Export devices data to Google Sheet.
82-
82+
8383
SECURITY NOTE: This export only includes non-sensitive device metadata:
8484
- Device names, status, targets, apps
8585
- VPN IP addresses (not secrets)
8686
- Creation dates
8787
- Production flags
88-
88+
8989
NO SECRETS EXPORTED:
9090
- No API keys, tokens, or passwords
9191
- No private keys or certificates
@@ -94,7 +94,7 @@ def export_devices_to_sheet(devices, worksheet, factory_name):
9494
if not devices:
9595
print("No devices to export")
9696
return
97-
97+
9898
# Define headers - ordered by importance/usefulness
9999
# SECURITY: Only include non-sensitive metadata
100100
headers = [
@@ -117,13 +117,13 @@ def export_devices_to_sheet(devices, worksheet, factory_name):
117117
"Days Since Created", # Calculated field
118118
"Days Since Last Seen", # Calculated field
119119
]
120-
120+
121121
# Prepare data rows
122122
rows = [headers]
123-
123+
124124
# Calculate days since created for each device
125125
from datetime import datetime as dt
126-
126+
127127
def parse_date(date_str):
128128
"""Parse date string from various formats."""
129129
if not date_str:
@@ -134,18 +134,18 @@ def parse_date(date_str):
134134
except ValueError:
135135
continue
136136
return None
137-
137+
138138
def days_since(date_str):
139139
"""Calculate days since a date string."""
140140
date_obj = parse_date(date_str)
141141
if date_obj:
142142
return str((dt.now() - date_obj).days)
143143
return ""
144-
144+
145145
for device in devices:
146146
created_at = device.get("created_at", "")
147147
last_seen = device.get("last_seen", "")
148-
148+
149149
row = [
150150
device.get("name", ""),
151151
device.get("status", ""),
@@ -167,11 +167,11 @@ def days_since(date_str):
167167
days_since(last_seen),
168168
]
169169
rows.append(row)
170-
170+
171171
# Clear existing content and add new data
172172
worksheet.clear()
173173
worksheet.update(values=rows, range_name="A1")
174-
174+
175175
# Calculate column letter for last column
176176
def col_letter(col_num):
177177
"""Convert column number (1-based) to letter (A, B, C, ..., Z, AA, AB, ...)"""
@@ -181,74 +181,83 @@ def col_letter(col_num):
181181
result = chr(65 + (col_num % 26)) + result
182182
col_num //= 26
183183
return result
184-
184+
185185
last_col = col_letter(len(headers))
186186
header_range = f"A1:{last_col}1"
187-
187+
188188
# Format header row (bold, colored background)
189-
worksheet.format(header_range, {
190-
"textFormat": {"bold": True},
191-
"backgroundColor": {"red": 0.2, "green": 0.6, "blue": 0.9},
192-
"horizontalAlignment": "CENTER",
193-
})
194-
189+
worksheet.format(
190+
header_range,
191+
{
192+
"textFormat": {"bold": True},
193+
"backgroundColor": {"red": 0.2, "green": 0.6, "blue": 0.9},
194+
"horizontalAlignment": "CENTER",
195+
},
196+
)
197+
195198
# Freeze header row for scrolling
196199
try:
197200
worksheet.freeze(rows=1)
198201
except Exception:
199202
pass # Freeze might not be available in all gspread versions
200-
203+
201204
# Add filters to header row (makes spreadsheet filterable and sortable)
202205
# Filters automatically enable sorting on all columns
203206
try:
204207
spreadsheet = worksheet.spreadsheet
205208
# Use the correct API format for setBasicFilter
206-
requests = [{
207-
"setBasicFilter": {
208-
"filter": {
209-
"range": {
210-
"sheetId": worksheet.id,
211-
"startRowIndex": 0,
212-
"endRowIndex": len(devices) + 1, # Include all data rows
213-
"startColumnIndex": 0,
214-
"endColumnIndex": len(headers),
209+
requests = [
210+
{
211+
"setBasicFilter": {
212+
"filter": {
213+
"range": {
214+
"sheetId": worksheet.id,
215+
"startRowIndex": 0,
216+
"endRowIndex": len(devices) + 1, # Include all data rows
217+
"startColumnIndex": 0,
218+
"endColumnIndex": len(headers),
219+
}
215220
}
216221
}
217222
}
218-
}]
223+
]
219224
spreadsheet.batch_update({"requests": requests})
220225
print("✅ Filters added - columns are now sortable and filterable")
221226
except Exception as e:
222227
print(f"⚠️ Warning: Could not add filters automatically: {e}")
223228
print(" You can add filters manually: Select header row > Data > Create a filter")
224229
print(" This will enable sorting and filtering on all columns")
225-
230+
226231
# Auto-resize columns
227232
try:
228233
worksheet.columns_auto_resize(0, len(headers))
229234
except Exception:
230235
pass # Auto-resize might not be available in all versions
231-
236+
232237
# Format specific columns for better readability
233238
if len(devices) > 0:
234239
# Status column - bold
235-
worksheet.format(f"B2:B{len(devices) + 1}", {
236-
"textFormat": {"bold": True},
237-
})
238-
240+
worksheet.format(
241+
f"B2:B{len(devices) + 1}",
242+
{
243+
"textFormat": {"bold": True},
244+
},
245+
)
246+
239247
# Up-to-Date column - bold
240-
worksheet.format(f"F2:F{len(devices) + 1}", {
241-
"textFormat": {"bold": True},
242-
})
243-
248+
worksheet.format(
249+
f"F2:F{len(devices) + 1}",
250+
{
251+
"textFormat": {"bold": True},
252+
},
253+
)
254+
244255
print(f"✅ Exported {len(devices)} devices to sheet '{worksheet.title}'")
245-
print(f"📊 Filters enabled on header row - click filter icons to filter/sort")
256+
print("📊 Filters enabled on header row - click filter icons to filter/sort")
246257

247258

248259
def main():
249-
parser = argparse.ArgumentParser(
250-
description="Export Foundries devices to Google Sheets"
251-
)
260+
parser = argparse.ArgumentParser(description="Export Foundries devices to Google Sheets")
252261
parser.add_argument(
253262
"--spreadsheet-id",
254263
help="Google Spreadsheet ID (if not provided, creates a new spreadsheet)",
@@ -267,24 +276,24 @@ def main():
267276
"--title",
268277
help="Title for new spreadsheet (if creating new)",
269278
)
270-
279+
271280
args = parser.parse_args()
272-
281+
273282
# Get devices data
274283
print(f"Fetching devices from factory '{args.factory}'...")
275284
result = list_foundries_devices(factory=args.factory)
276-
285+
277286
if not result.get("success"):
278287
print(f"ERROR: Failed to fetch devices: {result.get('error', 'Unknown error')}")
279288
sys.exit(1)
280-
289+
281290
devices = result.get("devices", [])
282291
print(f"✅ Retrieved {len(devices)} devices")
283-
292+
284293
if not devices:
285294
print("No devices to export")
286295
return
287-
296+
288297
# Authenticate with Google Sheets
289298
print("Authenticating with Google Sheets...")
290299
try:
@@ -298,36 +307,40 @@ def main():
298307
print("2. Create a project and enable Google Sheets API")
299308
print("3. Create a service account and download credentials JSON")
300309
print("4. Share your spreadsheet with the service account email")
301-
print("5. Set GOOGLE_SHEETS_CREDENTIALS environment variable or place credentials.json in project root")
310+
print(
311+
"5. Set GOOGLE_SHEETS_CREDENTIALS environment variable or place credentials.json in project root"
312+
)
302313
sys.exit(1)
303-
314+
304315
# Create or get spreadsheet
305316
if args.spreadsheet_id:
306317
print(f"Opening spreadsheet with ID: {args.spreadsheet_id}")
307318
spreadsheet = create_or_get_spreadsheet(gc, spreadsheet_id=args.spreadsheet_id)
308319
else:
309-
title = args.title or f"Foundries Devices - {args.factory} - {datetime.now().strftime('%Y-%m-%d')}"
320+
title = (
321+
args.title
322+
or f"Foundries Devices - {args.factory} - {datetime.now().strftime('%Y-%m-%d')}"
323+
)
310324
print(f"Creating new spreadsheet: {title}")
311325
spreadsheet = create_or_get_spreadsheet(gc, title=title)
312326
print(f"✅ Created spreadsheet: {spreadsheet.url}")
313-
327+
314328
# Get or create worksheet
315329
try:
316330
worksheet = spreadsheet.worksheet(args.sheet_name)
317331
print(f"Using existing sheet: {args.sheet_name}")
318332
except gspread.exceptions.WorksheetNotFound:
319333
worksheet = spreadsheet.add_worksheet(title=args.sheet_name, rows=1000, cols=20)
320334
print(f"Created new sheet: {args.sheet_name}")
321-
335+
322336
# Export data
323337
print(f"Exporting {len(devices)} devices to sheet...")
324338
export_devices_to_sheet(devices, worksheet, args.factory)
325-
326-
print(f"\n✅ Export complete!")
339+
340+
print("\n✅ Export complete!")
327341
print(f"📊 Spreadsheet URL: {spreadsheet.url}")
328342
print(f"📋 Sheet name: {worksheet.title}")
329343

330344

331345
if __name__ == "__main__":
332346
main()
333-

lab_testing/server/tool_handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def reload_lab_testing_modules():
6161
copy_files_to_device_parallel,
6262
sync_directory_to_device,
6363
)
64+
from lab_testing.tools.foundries_devices import list_foundries_devices
6465
from lab_testing.tools.foundries_vpn import (
6566
check_client_peer_registered,
6667
check_foundries_vpn_client_config,
@@ -76,7 +77,6 @@ def reload_lab_testing_modules():
7677
setup_foundries_vpn,
7778
verify_foundries_vpn_connection,
7879
)
79-
from lab_testing.tools.foundries_devices import list_foundries_devices
8080
from lab_testing.tools.ota_manager import (
8181
check_ota_status,
8282
deploy_container,

lab_testing/tools/foundries_devices.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,18 +160,18 @@ def list_foundries_devices(factory: Optional[str] = None) -> Dict[str, Any]:
160160
# Fixed positions: name[0], target[1], status[2], apps[3], up-to-date[4], is-prod[5]
161161
# created-at[6], last-seen[7], then variable: device-group[8] (may be empty), tag[8 or 9],
162162
# owner[9 or 10], updated-at[10 or 11] (may be empty), ostree-hash[10 or 11], uuid[11 or 12]
163-
163+
164164
if len(parts) >= 6:
165165
# At minimum: name, target, status, apps, up-to-date, is-prod
166166
up_to_date = parts[4].strip() if len(parts) > 4 else "unknown"
167167
is_prod = parts[5].strip() if len(parts) > 5 else "unknown"
168168
apps = parts[3].strip() if len(parts) > 3 else ""
169-
169+
170170
if len(parts) >= 8:
171171
# Has: name, target, status, apps, up-to-date, is-prod, created-at, last-seen
172172
created_at = parts[6].strip() if len(parts) > 6 else None
173173
last_seen = parts[7].strip() if len(parts) > 7 else None
174-
174+
175175
# For remaining fields, parse from the end (most reliable)
176176
# UUID is always last (if present), ostree-hash is second-to-last, etc.
177177
if len(parts) >= 12:
@@ -300,4 +300,3 @@ def list_foundries_devices(factory: Optional[str] = None) -> Dict[str, Any]:
300300
"Verify you have access to the factory",
301301
],
302302
}
303-

lab_testing/tools/foundries_vpn.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,7 @@ def enable_foundries_device_to_device(
16281628
# Get device IP if not provided
16291629
if not device_ip:
16301630
from lab_testing.tools.foundries_devices import list_foundries_devices
1631+
16311632
devices = list_foundries_devices()
16321633
if not devices.get("success"):
16331634
return {

0 commit comments

Comments
 (0)