From cf05e771e8f240a727a22a2291a0d99596f8e861 Mon Sep 17 00:00:00 2001 From: collin-koss Date: Mon, 29 Jun 2026 15:48:02 -0700 Subject: [PATCH 1/5] Add delete_system_info.py utility for cleaning up bugged multi-profile devices --- rename-hostnames/README.md | 78 +++++ rename-hostnames/delete_serials.csv | 3 + rename-hostnames/delete_system_info.py | 416 +++++++++++++++++++++++++ 3 files changed, 497 insertions(+) create mode 100644 rename-hostnames/delete_serials.csv create mode 100644 rename-hostnames/delete_system_info.py diff --git a/rename-hostnames/README.md b/rename-hostnames/README.md index fd4b7ca..1ad3f1a 100644 --- a/rename-hostnames/README.md +++ b/rename-hostnames/README.md @@ -108,6 +108,84 @@ serial1,hostname1,success seria2,hostname2,failure ``` +## Accompanying Utility Script: Delete System Info Profiles + +### Overview + +The `delete_system_info.py` script is a utility for fixing devices stuck in a bugged +state due to Central no longer supporting multiple local system-info profiles. Devices +follow the same provisioning requirements as laid out previosuly for renaming hostnames. + +**Background:** Devices can no longer have multiple local system-info profiles in +Central. Any device with multiple system-info profiles will be unable to create or +update these profiles until all extra profiles are removed (leaving zero or one +profile). This script cleans up the bugged state by deleting all local system-info +profiles for the target devices. + +### When to Use + +Use this utility when: +- A device fails to update its hostname with errors related to maximum/existing + system-info profiles. Example error: + ```json + { + "httpStatusCode": 400, + "message": "module aruba-system-info can only have single instance per scope", + "debugId": "axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "errorCode": "HPE_GL_ERROR_BAD_REQUEST" + } + ``` +- A device has multiple system-info profiles from legacy configurations +- You need to reset a device's system-info profile state before re-configuring + +### Input + +Create a CSV file with a `serial` column header containing target device serial numbers: + +```csv +serial +CNXXXXXXXX +CNXXXXXXX1 +``` + +By default, the script uses `delete_serials.csv`. Update this file or provide a custom CSV. + +### Execution + +```bash +python delete_system_info.py +``` + +With arguments: + +```bash +python delete_system_info.py -c --serials_csv +``` + +Or provide serial numbers directly: + +```bash +python delete_system_info.py --serials CNXXXXXXXX,CNXXXXXXX1,CNXXXXXXX2 +``` + +### Command Line Options + +| Name | Type | Description | Required | Default | +|-----------------|--------|---------------------------------------|----------|--------------------------| +| credential_file | string | Path to file with Central credentials | No | account_credentials.yaml | +| serials_csv | string | Path to CSV file with serial numbers | No | delete_serials.csv | +| serials | string | Comma-separated list of serials | No | None | + +### Output + +Results are saved to `delete_system_info_results.csv`: + +```csv +serial_number,device_function,profiles_deleted,status +CNXXXXXXXX,ACCESS_SWITCH,2,success +CNXXXXXXX1,AP,0,no_profiles +``` + ## Troubleshooting - Authentication / tokens: Ensure your credentials file is complete and has valid credentials for Central. diff --git a/rename-hostnames/delete_serials.csv b/rename-hostnames/delete_serials.csv new file mode 100644 index 0000000..932f325 --- /dev/null +++ b/rename-hostnames/delete_serials.csv @@ -0,0 +1,3 @@ +serial +CNXXXXXXXX +CNXXXXXXX1 diff --git a/rename-hostnames/delete_system_info.py b/rename-hostnames/delete_system_info.py new file mode 100644 index 0000000..dd562e2 --- /dev/null +++ b/rename-hostnames/delete_system_info.py @@ -0,0 +1,416 @@ +""" +Delete System Info Profiles + +This script deletes all system-info profiles from specified devices in Aruba Central. +It takes device serial numbers as input and removes all associated system-info profiles +for each device. + +Usage: + python delete_system_info.py -c account_credentials.yaml --serials_csv serials.csv + python delete_system_info.py -c account_credentials.yaml --serials SN1,SN2,SN3 +""" + +import csv +import sys +from argparse import ArgumentParser + +import yaml +from halo import Halo +from termcolor import colored + +from pycentral import NewCentralBase +from pycentral.profiles import Profiles +from pycentral.utils.url_utils import generate_url + +# Data structures to track results +serial_numbers = [] +device_functions = [] +profiles_deleted = [] +status = [] + +get_sys_info_path = generate_url("system-info") + + +def define_arguments(): + """Define command line arguments for the script. + + Returns: + argparse.Namespace: Parsed command line arguments + """ + description = ( + "This script deletes all system-info profiles from specified devices in Central" + ) + + parser = ArgumentParser(description=description) + parser.add_argument( + "-c", + "--credential_file", + help="Central API Authorization file path", + default="account_credentials.yaml", + ) + parser.add_argument( + "--serials_csv", + help="CSV file with serial numbers (column header: 'serial')", + default="delete_serials.csv", + ) + parser.add_argument( + "--serials", + help="Comma-separated list of device serial numbers", + default=None, + ) + + return parser.parse_args() + + +def load_credentials(file_path): + """Load credentials from YAML file. + + Args: + file_path: Path to the credentials YAML file + + Returns: + dict: Loaded credentials + """ + try: + with open(file_path, "r") as file: + credentials = yaml.safe_load(file) + return credentials + except FileNotFoundError: + print( + f"{colored('Error', 'red')} - Credentials file '{file_path}' not found.\n" + ) + sys.exit(1) + except yaml.YAMLError as e: + print( + f"{colored('Error', 'red')} - Error parsing YAML file '{file_path}': {e}\n" + ) + sys.exit(1) + + +def validate_csv(file_path): + """Validate the CSV file structure. + + Args: + file_path: Path to the CSV file + """ + try: + with open(file_path, newline="") as csvfile: + read = csv.reader(csvfile) + + try: + head = next(read) + except StopIteration: + print( + f"{colored('Error', 'red')} - CSV file is empty. Please add headers and data.\n" + ) + sys.exit(1) + + # Check for 'serial' header (case-insensitive) + headers_lower = [h.lower().strip() for h in head] + if "serial" not in headers_lower: + print(f"{colored('Error', 'red')} - CSV must have a 'serial' column.\n") + print(f" Found headers: {', '.join(head)}\n") + sys.exit(1) + + print(f"{colored('Success', 'green')} - Input CSV format validated \n") + + except FileNotFoundError: + print(f"{colored('Error', 'red')} - CSV file '{file_path}' not found.\n") + sys.exit(1) + + +def read_csv(file_path): + """Read serial numbers from CSV file. + + Args: + file_path: Path to the CSV file + """ + with open(file_path, "r") as csv_file: + csv_reader = csv.DictReader(csv_file) + + for row in csv_reader: + # Get serial column (case-insensitive) + serial = None + for key in row: + if key.lower().strip() == "serial": + serial = row[key].strip() + break + if serial: + serial_numbers.append(serial) + + +def parse_serial_list(serials_str): + """Parse comma-separated serial numbers. + + Args: + serials_str: Comma-separated string of serial numbers + """ + for serial in serials_str.split(","): + serial = serial.strip() + if serial: + serial_numbers.append(serial) + + +def check_device(scope, serial_number): + """Check if device exists and is provisioned in Central. + + Args: + scope: PyCentral scope object + serial_number: Device serial number + + Returns: + tuple: (device_object, device_function) or (None, None) if not found + """ + spinner = Halo(text="Checking for device in Central...", spinner="simpleDots") + spinner.start() + + device_object = scope.find_device(device_serials=serial_number) + device_function = getattr(device_object, "config_persona", None) + provisioned = getattr(device_object, "provisioned_status", None) + + if not device_object: + spinner.fail() + print( + f" {colored('Error', 'red')}: Device {colored(serial_number, 'blue')} not found in Central.\n" + ) + status.append("not_found") + device_functions.append(None) + profiles_deleted.append(0) + return None, None + + if not provisioned: + spinner.fail() + print( + f" {colored('Error', 'red')}: Device {colored(serial_number, 'blue')} not provisioned in Central.\n" + ) + status.append("not_provisioned") + device_functions.append(None) + profiles_deleted.append(0) + return None, None + + if not device_function: + spinner.fail() + print( + f" {colored('Error', 'red')}: Device {colored(serial_number, 'blue')} has no persona assigned.\n" + ) + status.append("no_persona") + device_functions.append(None) + profiles_deleted.append(0) + return None, None + + spinner.succeed() + print( + f" Device {colored(serial_number, 'blue')} found with persona: {colored(device_function, 'cyan')}\n" + ) + device_functions.append(device_function) + return device_object, device_function + + +def get_system_info_profiles(central_conn, scope_id, persona): + """Retrieve all system-info profiles for a device. + + Args: + central_conn: PyCentral connection object + scope_id: Device scope ID + persona: Device function/persona + + Returns: + list: List of profile names + """ + local = {"scope_id": scope_id, "persona": persona} + response = Profiles.get_profile(get_sys_info_path, central_conn, local=local) + + profiles = [] + if response[0] and response[1]: + # response[1] contains the profile data + data = response[1] + if isinstance(data, dict) and "profile" in data: + # Extract profile names from the "profile" array + profile_data = data["profile"] + if isinstance(profile_data, list): + for profile in profile_data: + if isinstance(profile, dict) and "name" in profile: + profiles.append(profile["name"]) + + return profiles + + +def delete_system_info_profile(central_conn, profile_name, scope_id, persona): + """Delete a specific system-info profile. + + Args: + central_conn: PyCentral connection object + profile_name: Name of the profile to delete + scope_id: Device scope ID + persona: Device function/persona + + Returns: + bool: True if deletion was successful + """ + # Generate delete path with profile name appended + delete_sys_info_path = generate_url(f"system-info/{profile_name}") + local = {"scope_id": scope_id, "persona": persona} + response = Profiles.delete_profile(delete_sys_info_path, central_conn, local=local) + + if response[0]: + return True + return False + + +def delete_all_profiles(central_conn, serial_number, device_object, persona): + """Delete all system-info profiles for a device. + + Args: + central_conn: PyCentral connection object + serial_number: Device serial number + device_object: PyCentral device object + persona: Device function/persona + scope: PyCentral scope object + """ + scope_id = getattr(device_object, "id", None) + if not scope_id: + print(f" {colored('Error', 'red')}: Could not get scope ID for device.\n") + status.append("error") + profiles_deleted.append(0) + return + + deleted_count = 0 + iteration = 1 + max_iterations = 50 # Safety limit to prevent infinite loops + + while iteration <= max_iterations: + spinner = Halo( + text=f"Fetching system-info profiles (iteration {iteration})...", + spinner="simpleDots", + ) + spinner.start() + + profiles = get_system_info_profiles(central_conn, scope_id, persona) + + if not profiles: + spinner.succeed() + if deleted_count == 0: + print( + f" No system-info profiles found for device {colored(serial_number, 'blue')}.\n" + ) + else: + print( + f" All system-info profiles deleted for device {colored(serial_number, 'blue')}.\n" + ) + break + + spinner.succeed() + print(f" Found {len(profiles)} profile(s): {', '.join(profiles)}") + + # Delete each profile + for profile_name in profiles: + del_spinner = Halo( + text=f"Deleting profile '{profile_name}'...", spinner="simpleDots" + ) + del_spinner.start() + + success = delete_system_info_profile( + central_conn, profile_name, scope_id, persona + ) + + if success: + del_spinner.succeed() + print(f" Successfully deleted profile: {colored(profile_name, 'magenta')}") + deleted_count += 1 + else: + del_spinner.fail() + print(f" {colored('Failed', 'red')} to delete profile: {profile_name}") + + iteration += 1 + + if iteration > max_iterations: + print( + f" {colored('Warning', 'yellow')}: Reached maximum iterations. Some profiles may remain.\n" + ) + + profiles_deleted.append(deleted_count) + if deleted_count > 0: + status.append("success") + else: + status.append("no profiles") + + print() + + +def create_output(output_file): + """Create output CSV with results. + + Args: + output_file: Path to output CSV file + """ + data = zip(serial_numbers, device_functions, profiles_deleted, status) + + with open(output_file, "w", newline="") as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["serial_number", "device_function", "profiles_deleted", "status"]) + writer.writerows(data) + + +def main(): + """Main function to orchestrate the profile deletion workflow.""" + args = define_arguments() + + credentials_file = args.credential_file + credentials = load_credentials(credentials_file) + + # Initialize Central connection + print("Connecting to Central & fetching hierarchy information...") + try: + central_conn = NewCentralBase( + token_info=credentials, + log_level="CRITICAL", + enable_scope=True, + ) + print(f"{colored('Success', 'green')} - Connected to Central\n") + except Exception as e: + print(f"\n{colored('Error', 'red')}: {e}\n") + sys.exit(1) + + scope = central_conn.scopes + + # Load target serials + if args.serials: + parse_serial_list(args.serials) + else: + validate_csv(args.serials_csv) + read_csv(args.serials_csv) + + if not serial_numbers: + print(f"{colored('Error', 'red')} - No serial numbers found in input.\n") + sys.exit(1) + + print(f"Processing {len(serial_numbers)} device(s)...\n") + print("=" * 60) + + # Process device(s) + for device in serial_numbers: + print(f"\nDevice: {colored(device, 'blue')}") + print("-" * 40) + + device_object, persona = check_device(scope, device) + if device_object and persona: + delete_all_profiles(central_conn, device, device_object, persona) + + # Print summary table + print("=" * 60) + print("\nSummary:") + print("| Serial Number | Device Function | Profiles Deleted | Status |") + print("+---------------+-----------------+------------------+--------+") + + for sn, df, pd, st in zip(serial_numbers, device_functions, profiles_deleted, status): + df_str = df if df else "N/A" + print(f"| {sn:^13} | {df_str:^15} | {pd:^16} | {st:^6} |") + + # Create output CSV + csv_output_name = "delete_system_info_results.csv" + create_output(csv_output_name) + print(f"\nResults saved to {colored(csv_output_name, 'cyan')}\n") + + +if __name__ == "__main__": + main() From 77bb5532b83b1143cd8a03205934aa1c0e79a1e6 Mon Sep 17 00:00:00 2001 From: collin-koss Date: Mon, 29 Jun 2026 16:10:03 -0700 Subject: [PATCH 2/5] Update script formatting and documentation. --- rename-hostnames/delete_system_info.py | 60 +++++++++----------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/rename-hostnames/delete_system_info.py b/rename-hostnames/delete_system_info.py index dd562e2..b485950 100644 --- a/rename-hostnames/delete_system_info.py +++ b/rename-hostnames/delete_system_info.py @@ -1,15 +1,3 @@ -""" -Delete System Info Profiles - -This script deletes all system-info profiles from specified devices in Aruba Central. -It takes device serial numbers as input and removes all associated system-info profiles -for each device. - -Usage: - python delete_system_info.py -c account_credentials.yaml --serials_csv serials.csv - python delete_system_info.py -c account_credentials.yaml --serials SN1,SN2,SN3 -""" - import csv import sys from argparse import ArgumentParser @@ -22,7 +10,6 @@ from pycentral.profiles import Profiles from pycentral.utils.url_utils import generate_url -# Data structures to track results serial_numbers = [] device_functions = [] profiles_deleted = [] @@ -66,7 +53,7 @@ def load_credentials(file_path): """Load credentials from YAML file. Args: - file_path: Path to the credentials YAML file + file_path (str): Path to the credentials YAML file Returns: dict: Loaded credentials @@ -91,7 +78,7 @@ def validate_csv(file_path): """Validate the CSV file structure. Args: - file_path: Path to the CSV file + file_path (str): Path to the CSV file """ try: with open(file_path, newline="") as csvfile: @@ -105,7 +92,7 @@ def validate_csv(file_path): ) sys.exit(1) - # Check for 'serial' header (case-insensitive) + # Check for header formatting headers_lower = [h.lower().strip() for h in head] if "serial" not in headers_lower: print(f"{colored('Error', 'red')} - CSV must have a 'serial' column.\n") @@ -123,13 +110,12 @@ def read_csv(file_path): """Read serial numbers from CSV file. Args: - file_path: Path to the CSV file + file_path (str): Path to the CSV file """ with open(file_path, "r") as csv_file: csv_reader = csv.DictReader(csv_file) for row in csv_reader: - # Get serial column (case-insensitive) serial = None for key in row: if key.lower().strip() == "serial": @@ -143,7 +129,7 @@ def parse_serial_list(serials_str): """Parse comma-separated serial numbers. Args: - serials_str: Comma-separated string of serial numbers + serials_str (str): Comma-separated string of serial numbers """ for serial in serials_str.split(","): serial = serial.strip() @@ -155,8 +141,8 @@ def check_device(scope, serial_number): """Check if device exists and is provisioned in Central. Args: - scope: PyCentral scope object - serial_number: Device serial number + scope (pycentral.workflows.workflows.Scopes): PyCentral scope object + serial_number (str): Device serial number Returns: tuple: (device_object, device_function) or (None, None) if not found @@ -210,9 +196,9 @@ def get_system_info_profiles(central_conn, scope_id, persona): """Retrieve all system-info profiles for a device. Args: - central_conn: PyCentral connection object - scope_id: Device scope ID - persona: Device function/persona + central_conn (pycentral.NewCentralBase): PyCentral connection object + scope_id (int): Device scope ID + persona (str): Device function/persona Returns: list: List of profile names @@ -222,10 +208,9 @@ def get_system_info_profiles(central_conn, scope_id, persona): profiles = [] if response[0] and response[1]: - # response[1] contains the profile data data = response[1] if isinstance(data, dict) and "profile" in data: - # Extract profile names from the "profile" array + # Extract profile names profile_data = data["profile"] if isinstance(profile_data, list): for profile in profile_data: @@ -239,15 +224,14 @@ def delete_system_info_profile(central_conn, profile_name, scope_id, persona): """Delete a specific system-info profile. Args: - central_conn: PyCentral connection object - profile_name: Name of the profile to delete - scope_id: Device scope ID - persona: Device function/persona + central_conn (pycentral.NewCentralBase): PyCentral connection object + profile_name (str): Name of the profile to delete + scope_id (int): Device scope ID + persona (str): Device function/persona Returns: bool: True if deletion was successful """ - # Generate delete path with profile name appended delete_sys_info_path = generate_url(f"system-info/{profile_name}") local = {"scope_id": scope_id, "persona": persona} response = Profiles.delete_profile(delete_sys_info_path, central_conn, local=local) @@ -261,11 +245,10 @@ def delete_all_profiles(central_conn, serial_number, device_object, persona): """Delete all system-info profiles for a device. Args: - central_conn: PyCentral connection object - serial_number: Device serial number - device_object: PyCentral device object - persona: Device function/persona - scope: PyCentral scope object + central_conn (pycentral.NewCentralBase): PyCentral connection object + serial_number (str): Device serial number + device_object (pycentral.workflows.workflows.Device): PyCentral device object + persona (str): Device function/persona """ scope_id = getattr(device_object, "id", None) if not scope_id: @@ -276,7 +259,7 @@ def delete_all_profiles(central_conn, serial_number, device_object, persona): deleted_count = 0 iteration = 1 - max_iterations = 50 # Safety limit to prevent infinite loops + max_iterations = 50 while iteration <= max_iterations: spinner = Halo( @@ -341,7 +324,7 @@ def create_output(output_file): """Create output CSV with results. Args: - output_file: Path to output CSV file + output_file (str): Path to output CSV file """ data = zip(serial_numbers, device_functions, profiles_deleted, status) @@ -358,7 +341,6 @@ def main(): credentials_file = args.credential_file credentials = load_credentials(credentials_file) - # Initialize Central connection print("Connecting to Central & fetching hierarchy information...") try: central_conn = NewCentralBase( From 7788d9fc2bf6a90cae448fc497cc8213db8554b7 Mon Sep 17 00:00:00 2001 From: collin-koss Date: Mon, 29 Jun 2026 16:12:29 -0700 Subject: [PATCH 3/5] Update troubleshooting section. --- rename-hostnames/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/rename-hostnames/README.md b/rename-hostnames/README.md index 1ad3f1a..4aad106 100644 --- a/rename-hostnames/README.md +++ b/rename-hostnames/README.md @@ -192,6 +192,7 @@ CNXXXXXXX1,AP,0,no_profiles - Ensure all target devices have been assigned a device function and are ready for provisioning - Ensure hostnames are a valid format for the device type they are attempting to be assigned to - SDK compatibility: If API calls fail unexpectedly, confirm the installed pycentral version matches tested versions (v2.0a16) or update helpers accordingly. +- If unable to update/create hostnames review Delete System Info utility script ## Support From 3007490b48dad6a00ff4f1e3d5d50cd0381e8a8d Mon Sep 17 00:00:00 2001 From: collin-koss Date: Mon, 29 Jun 2026 16:14:29 -0700 Subject: [PATCH 4/5] Supported version troubleshooting update --- rename-hostnames/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rename-hostnames/README.md b/rename-hostnames/README.md index 4aad106..c71b20a 100644 --- a/rename-hostnames/README.md +++ b/rename-hostnames/README.md @@ -191,7 +191,7 @@ CNXXXXXXX1,AP,0,no_profiles - Authentication / tokens: Ensure your credentials file is complete and has valid credentials for Central. - Ensure all target devices have been assigned a device function and are ready for provisioning - Ensure hostnames are a valid format for the device type they are attempting to be assigned to -- SDK compatibility: If API calls fail unexpectedly, confirm the installed pycentral version matches tested versions (v2.0a16) or update helpers accordingly. +- SDK compatibility: If API calls fail unexpectedly, confirm the installed pycentral version matches tested versions (2.0a21) or update helpers accordingly. - If unable to update/create hostnames review Delete System Info utility script ## Support From 63c2f24cd9a1b7f7d73759fe76f94b847b4718d9 Mon Sep 17 00:00:00 2001 From: collin-koss Date: Tue, 30 Jun 2026 15:21:39 -0700 Subject: [PATCH 5/5] Refactor script to track status as partial in addition to succes/failed. Updated looping logic to attempt delete once on every profile, tracking failures. --- rename-hostnames/delete_system_info.py | 99 +++++++++++++------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/rename-hostnames/delete_system_info.py b/rename-hostnames/delete_system_info.py index b485950..b150fa4 100644 --- a/rename-hostnames/delete_system_info.py +++ b/rename-hostnames/delete_system_info.py @@ -257,65 +257,66 @@ def delete_all_profiles(central_conn, serial_number, device_object, persona): profiles_deleted.append(0) return - deleted_count = 0 - iteration = 1 - max_iterations = 50 + spinner = Halo( + text="Fetching system-info profiles...", + spinner="simpleDots", + ) + spinner.start() + + profiles = get_system_info_profiles(central_conn, scope_id, persona) - while iteration <= max_iterations: - spinner = Halo( - text=f"Fetching system-info profiles (iteration {iteration})...", - spinner="simpleDots", + if not profiles: + spinner.succeed() + print( + f" No system-info profiles found for device {colored(serial_number, 'blue')}.\n" ) - spinner.start() + profiles_deleted.append(0) + status.append("no profiles") + return - profiles = get_system_info_profiles(central_conn, scope_id, persona) + spinner.succeed() + print(f" Found {len(profiles)} profile(s): {', '.join(profiles)}") - if not profiles: - spinner.succeed() - if deleted_count == 0: - print( - f" No system-info profiles found for device {colored(serial_number, 'blue')}.\n" - ) - else: - print( - f" All system-info profiles deleted for device {colored(serial_number, 'blue')}.\n" - ) - break + deleted_count = 0 + failed_profiles = [] - spinner.succeed() - print(f" Found {len(profiles)} profile(s): {', '.join(profiles)}") - - # Delete each profile - for profile_name in profiles: - del_spinner = Halo( - text=f"Deleting profile '{profile_name}'...", spinner="simpleDots" - ) - del_spinner.start() - - success = delete_system_info_profile( - central_conn, profile_name, scope_id, persona - ) - - if success: - del_spinner.succeed() - print(f" Successfully deleted profile: {colored(profile_name, 'magenta')}") - deleted_count += 1 - else: - del_spinner.fail() - print(f" {colored('Failed', 'red')} to delete profile: {profile_name}") - - iteration += 1 - - if iteration > max_iterations: - print( - f" {colored('Warning', 'yellow')}: Reached maximum iterations. Some profiles may remain.\n" + for profile_name in profiles: + del_spinner = Halo( + text=f"Deleting profile '{profile_name}'...", spinner="simpleDots" ) + del_spinner.start() + + success = delete_system_info_profile( + central_conn, profile_name, scope_id, persona + ) + + if success: + del_spinner.succeed() + print(f" Successfully deleted profile: {colored(profile_name, 'magenta')}") + deleted_count += 1 + else: + del_spinner.fail() + print(f" {colored('Failed', 'red')} to delete profile: {profile_name}") + failed_profiles.append(profile_name) profiles_deleted.append(deleted_count) - if deleted_count > 0: + + # Determine status + if deleted_count > 0 and not failed_profiles: status.append("success") + print( + f" All system-info profiles deleted for device {colored(serial_number, 'blue')}.\n" + ) + elif deleted_count > 0 and failed_profiles: + status.append("partial") + print( + f" {colored('Warning', 'yellow')}: {len(failed_profiles)} profile(s) failed to delete: {', '.join(failed_profiles)}\n" + ) else: - status.append("no profiles") + status.append("failed") + print( + f" {colored('Error', 'red')}: All deletions failed for device {colored(serial_number, 'blue')}.\n" + ) print()