Skip to content

Commit eb7a8ae

Browse files
committed
Added update_subscriptions
1 parent 8081db4 commit eb7a8ae

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

scripts/update_subscriptions.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Update Subscriptions - Standalone Script
4+
5+
Reads a CSV file with device identifiers and subscription IDs, then applies
6+
or regrades subscriptions to the corresponding devices in NCM.
7+
8+
Devices are processed in chunks of 100 and grouped by subscription ID for
9+
efficient batch processing.
10+
11+
Usage:
12+
Double-click on Windows, or run: python update_subscriptions.py
13+
Optional args: python update_subscriptions.py [csv_filename] [token]
14+
15+
CSV Format:
16+
Required columns (case-insensitive):
17+
- mac: Device MAC address
18+
- subscription_id: Subscription ID (or "subscription")
19+
20+
MAC column variants: "mac", "mac address", "mac_address", "macaddress"
21+
22+
Example CSV:
23+
mac,subscription_id
24+
00:30:44:1A:2B:3C,BA-NCADV
25+
003044AABBCC,BA-NCADV
26+
27+
Configuration:
28+
Set your API token below, or as an environment variable.
29+
"""
30+
31+
import os
32+
import sys
33+
import csv
34+
35+
script_dir = os.path.dirname(os.path.abspath(__file__))
36+
37+
try:
38+
from ncm import ncm
39+
except ImportError:
40+
print("Error: 'ncm' library not found. Install it with: pip install ncm")
41+
input("Press Enter to exit...")
42+
sys.exit(1)
43+
44+
# ============================================================================
45+
# CONFIGURATION - Set your API token here
46+
# ============================================================================
47+
48+
TOKEN = "" # APIv3 token (or set TOKEN / NCM_API_TOKEN env var, or pass as 2nd arg)
49+
50+
CSV_FILENAME = "devices.csv" # Default CSV filename
51+
52+
# ============================================================================
53+
54+
55+
def load_token():
56+
"""Load APIv3 token from arg, config, or environment variables."""
57+
if len(sys.argv) >= 3:
58+
return sys.argv[2]
59+
return TOKEN or os.environ.get("TOKEN") or os.environ.get("NCM_API_TOKEN") or os.environ.get("token")
60+
61+
62+
def find_column(fieldnames, candidates):
63+
"""Find a column from common name variants, handling BOM and whitespace."""
64+
normalized = {
65+
col.lstrip('\ufeff').lower().strip().replace(" ", "").replace("_", ""): col
66+
for col in fieldnames
67+
}
68+
for name in candidates:
69+
key = name.lower().strip().replace(" ", "").replace("_", "")
70+
if key in normalized:
71+
return normalized[key]
72+
return None
73+
74+
75+
def chunks(lst, n):
76+
"""Yield successive n-sized chunks from lst."""
77+
for i in range(0, len(lst), n):
78+
yield lst[i:i + n]
79+
80+
81+
def main():
82+
# Resolve CSV path
83+
csv_filename = sys.argv[1] if len(sys.argv) > 1 else CSV_FILENAME
84+
filepath = os.path.join(script_dir, csv_filename) if not os.path.isabs(csv_filename) else csv_filename
85+
86+
if not os.path.exists(filepath):
87+
print(f"Error: File not found: {filepath}")
88+
input("Press Enter to exit...")
89+
sys.exit(1)
90+
91+
# Load and validate APIv3 token
92+
token = load_token()
93+
if not token:
94+
print("Error: No APIv3 token found. Set TOKEN in the script or as an environment variable.")
95+
input("Press Enter to exit...")
96+
sys.exit(1)
97+
98+
# Initialize v3 client for regrades
99+
ncm_v3 = ncm.NcmClientv3(api_key=token, log_events=True)
100+
101+
# Read CSV
102+
with open(filepath, "r", newline="", encoding="utf-8") as f:
103+
reader = csv.DictReader(f)
104+
fieldnames = reader.fieldnames
105+
if not fieldnames:
106+
print("Error: CSV file is empty or has no headers.")
107+
input("Press Enter to exit...")
108+
sys.exit(1)
109+
110+
# Find MAC column
111+
mac_col = find_column(fieldnames, ['mac', 'mac address', 'mac_address', 'macaddress'])
112+
if not mac_col:
113+
print(f"Error: No MAC address column found. Available: {', '.join(fieldnames)}")
114+
print("Expected one of: mac, mac address, mac_address (case-insensitive)")
115+
input("Press Enter to exit...")
116+
sys.exit(1)
117+
118+
# Find subscription column
119+
sub_col = find_column(fieldnames, ['subscription_id', 'subscription', 'subscription id'])
120+
if not sub_col:
121+
print(f"Error: No subscription_id column found. Available: {', '.join(fieldnames)}")
122+
input("Press Enter to exit...")
123+
sys.exit(1)
124+
125+
rows = list(reader)
126+
127+
print(f"Processing {len(rows)} rows...", flush=True)
128+
129+
devices = []
130+
for i, row in enumerate(rows, 1):
131+
sub_id = row[sub_col].strip()
132+
mac = row[mac_col].strip()
133+
if not sub_id or not mac:
134+
print(f" Row {i}: Skipping, missing mac or subscription_id")
135+
continue
136+
mac = mac.lower().replace(':', '')
137+
devices.append({'mac': mac, 'subscription_id': sub_id})
138+
139+
print(f"\nFound {len(devices)} devices to regrade", flush=True)
140+
141+
if not devices:
142+
print("No devices to process.")
143+
input("Press Enter to exit...")
144+
sys.exit(0)
145+
146+
# Group by subscription_id
147+
subscription_groups = {}
148+
for device in devices:
149+
sub_id = device['subscription_id']
150+
if sub_id not in subscription_groups:
151+
subscription_groups[sub_id] = []
152+
subscription_groups[sub_id].append(device['mac'])
153+
154+
# Process regrades in chunks of 100
155+
for subscription_id, mac_addresses in subscription_groups.items():
156+
print(f"\nProcessing {len(mac_addresses)} devices with subscription: {subscription_id}")
157+
for chunk in chunks(mac_addresses, 100):
158+
try:
159+
result = ncm_v3.regrade(subscription_id, chunk)
160+
print(f" Chunk result: {result}")
161+
except Exception as e:
162+
print(f" Error: {e}")
163+
164+
print("\nDone!", flush=True)
165+
input("Press Enter to exit...")
166+
167+
168+
if __name__ == "__main__":
169+
main()

0 commit comments

Comments
 (0)