|
| 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