centralcli 2.3.0__tar.gz → 2.3.1__tar.gz

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.
Files changed (50) hide show
  1. {centralcli-2.3.0 → centralcli-2.3.1}/PKG-INFO +1 -1
  2. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/central.py +85 -0
  3. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cliassign.py +21 -10
  4. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clibatch.py +149 -27
  5. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clishow.py +8 -0
  6. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cliunassign.py +15 -2
  7. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/config.py +5 -0
  8. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/constants.py +1 -0
  9. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/strings.py +2 -0
  10. {centralcli-2.3.0 → centralcli-2.3.1}/pyproject.toml +1 -1
  11. {centralcli-2.3.0 → centralcli-2.3.1}/LICENSE +0 -0
  12. {centralcli-2.3.0 → centralcli-2.3.1}/README.md +0 -0
  13. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/__init__.py +0 -0
  14. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/boilerplate/README.md +0 -0
  15. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/boilerplate/allcalls.py +0 -0
  16. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/caas.py +0 -0
  17. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cache.py +0 -0
  18. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cleaner.py +0 -0
  19. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cli.py +0 -0
  20. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cliadd.py +0 -0
  21. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clicaas.py +0 -0
  22. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cliclone.py +0 -0
  23. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clicommon.py +0 -0
  24. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clidel.py +0 -0
  25. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clikick.py +0 -0
  26. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clirefresh.py +0 -0
  27. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clirename.py +0 -0
  28. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clishowbranch.py +0 -0
  29. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clishowfirmware.py +0 -0
  30. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clishowospf.py +0 -0
  31. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clishowoverlay.py +0 -0
  32. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clishowtshoot.py +0 -0
  33. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clishowwids.py +0 -0
  34. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clitest.py +0 -0
  35. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/clitshoot.py +0 -0
  36. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cliupdate.py +0 -0
  37. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/cliupgrade.py +0 -0
  38. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/exceptions.py +0 -0
  39. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/logger.py +0 -0
  40. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/models.py +0 -0
  41. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/objects.py +0 -0
  42. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/render.py +0 -0
  43. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/response.py +0 -0
  44. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/setup.py +0 -0
  45. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/static/favicon.ico +0 -0
  46. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/utils.py +0 -0
  47. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/vscodeargs.py +0 -0
  48. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/wh2snow.py +0 -0
  49. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/wh_proxy.py +0 -0
  50. {centralcli-2.3.0 → centralcli-2.3.1}/centralcli/wh_proxy_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: centralcli
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: A CLI for interacting with Aruba Central (Cloud Management Platform). Facilitates bulk imports, exports, reporting. A handy tool if you have devices managed by Aruba Central.
5
5
  Home-page: https://github.com/Pack3tL0ss/central-api-cli
6
6
  License: MIT
@@ -4028,6 +4028,25 @@ class CentralApi(Session):
4028
4028
  else:
4029
4029
  return await self.post(url, json_data=json_data)
4030
4030
 
4031
+ async def cop_delete_device_from_inventory(
4032
+ self,
4033
+ devices: List[str] = None,
4034
+ ) -> Response:
4035
+ """Delete devices using Serial number. Only applies to CoP deployments.
4036
+
4037
+ Args:
4038
+ devices (list, optional): List of devices to be deleted from
4039
+ GreenLake inventory. Only applies to CoP
4040
+
4041
+ Returns:
4042
+ Response: CentralAPI Response object
4043
+ """
4044
+ url = "/platform/device_inventory/v1/devices"
4045
+
4046
+ devices = [{"serial": serial} for serial in devices]
4047
+
4048
+ return await self.delete(url, json_data=devices)
4049
+
4031
4050
  # TODO maybe helper method to delete_device that calls these
4032
4051
  async def delete_gateway(
4033
4052
  self,
@@ -5477,6 +5496,72 @@ class CentralApi(Session):
5477
5496
 
5478
5497
  return await self.get(url)
5479
5498
 
5499
+
5500
+ async def get_auto_subscribe(
5501
+ self,
5502
+ ) -> Response:
5503
+ """Get the services which have auto subscribe enabled.
5504
+
5505
+ Returns:
5506
+ Response: CentralAPI Response object
5507
+ """
5508
+ url = "/platform/licensing/v1/customer/settings/autolicense"
5509
+
5510
+ return await self.get(url)
5511
+
5512
+ async def enable_auto_subscribe(
5513
+ self,
5514
+ services: List[str] | str,
5515
+ ) -> Response:
5516
+ """Standalone Customer API: Assign licenses to all devices and enable auto subscribe for
5517
+ given services.
5518
+
5519
+ Args:
5520
+ services (List[str]): list of services e.g. ['pa', 'ucc', foundation_ap,
5521
+ advanced_switch_6200, foundation_70XX etc ...]. Check
5522
+ /platform/licensing/v1/services/config API response to know the list of supported
5523
+ services.
5524
+
5525
+ Returns:
5526
+ Response: CentralAPI Response object
5527
+ """
5528
+ url = "/platform/licensing/v1/customer/settings/autolicense"
5529
+
5530
+ if isinstance(services, str):
5531
+ services = [services]
5532
+
5533
+ json_data = {
5534
+ 'services': services
5535
+ }
5536
+
5537
+ return await self.post(url, json_data=json_data)
5538
+
5539
+ async def disable_auto_subscribe(
5540
+ self,
5541
+ services: List[str] | str,
5542
+ ) -> Response:
5543
+ """Standalone Customer API: Disable auto licensing for given services.
5544
+
5545
+ Args:
5546
+ services (List[str]): list of services e.g. ['pa', 'ucc', foundation_ap,
5547
+ advanced_switch_6200, foundation_70XX etc ...]. Check
5548
+ /platform/licensing/v1/services/config API response to know the list of supported
5549
+ services.
5550
+
5551
+ Returns:
5552
+ Response: CentralAPI Response object
5553
+ """
5554
+ url = "/platform/licensing/v1/customer/settings/autolicense"
5555
+
5556
+ if isinstance(services, str):
5557
+ services = [services]
5558
+
5559
+ json_data = {
5560
+ 'services': services
5561
+ }
5562
+
5563
+ return await self.delete(url, json_data=json_data)
5564
+
5480
5565
  # // -- Not used by commands yet. undocumented kms api -- //
5481
5566
  async def kms_get_synced_aps(self, mac: str) -> Response:
5482
5567
  url = f"/keymgmt/v1/syncedaplist/{mac}"
@@ -24,10 +24,10 @@ except (ImportError, ModuleNotFoundError) as e:
24
24
  app = typer.Typer()
25
25
 
26
26
 
27
- @app.command(short_help="Assign License to device(s)", hidden=False)
27
+ @app.command()
28
28
  def license(
29
29
  license: cli.cache.LicenseTypes = typer.Argument(..., show_default=False),
30
- serial_nums: List[str] = typer.Argument(..., show_default=False),
30
+ serial_nums: List[str] = typer.Argument(..., help="device serial numbers or 'auto' to enable auto-subscribe.", show_default=False),
31
31
  yes: bool = typer.Option(False, "-Y", "-y", help="Bypass confirmation prompts - Assume Yes"),
32
32
  debug: bool = typer.Option(False, "--debug", envvar="ARUBACLI_DEBUG", help="Enable Additional Debug Logging",),
33
33
  default: bool = typer.Option(False, "-d", is_flag=True, help="Use default central account", show_default=False,),
@@ -36,7 +36,7 @@ def license(
36
36
  help="The Aruba Central Account to use (must be defined in the config)",
37
37
  autocompletion=cli.cache.account_completion),
38
38
  ) -> None:
39
- """Assign Licenses to devices by serial number.
39
+ """Assign Licenses to devices by serial number(s) or enable auto-subscribe for the license type.
40
40
 
41
41
  Device must already be added to Central. Use '[cyan]cencli show inventory[/]' to see devices that have been added.
42
42
  Use '--license' option with '[cyan]cencli add device ...[/]' to add device and assign license in one command.
@@ -44,16 +44,27 @@ def license(
44
44
  # devices = [cli.cache.get_dev_identifier(dev) for dev in devices]
45
45
 
46
46
  # TODO add confirmation method builder to output class
47
- _msg = f"Assign [bright_green]{license}[/bright_green] to"
48
- if len(serial_nums) > 1:
49
- _dev_msg = '\n '.join([f'[cyan]{dev}[/]' for dev in serial_nums])
50
- _msg = f"{_msg}:\n {_dev_msg}"
47
+ do_auto = True if "auto" in [s.lower() for s in serial_nums] else False
48
+ if do_auto:
49
+ _msg = f"Enable Auto-assignment of [bright_green]{license}[/bright_green] to applicable devices."
50
+ if len(serial_nums) > 1:
51
+ print(f'[cyan]auto[/] keyword provided remaining entries will be [bright_red]ignored[/]')
51
52
  else:
52
- dev = serial_nums[0]
53
- _msg = f"{_msg} [cyan]{dev}[/]"
53
+ _msg = f"Assign [bright_green]{license}[/bright_green] to"
54
+ if len(serial_nums) > 1:
55
+ _dev_msg = '\n '.join([f'[cyan]{dev}[/]' for dev in serial_nums])
56
+ _msg = f"{_msg}:\n {_dev_msg}"
57
+ else:
58
+ dev = serial_nums[0]
59
+ _msg = f"{_msg} [cyan]{dev}[/]"
60
+
54
61
  print(_msg)
55
62
  if yes or typer.confirm("\nProceed?"):
56
- resp = cli.central.request(cli.central.assign_licenses, serial_nums, services=license.name)
63
+ if not do_auto:
64
+ resp = cli.central.request(cli.central.assign_licenses, serial_nums, services=license.name)
65
+ else:
66
+ resp = cli.central.request(cli.central.enable_auto_subscribe, services=license.name)
67
+
57
68
  cli.display_results(resp, tablefmt="action")
58
69
  # TODO cache update similar to batch unsubscribe
59
70
 
@@ -1011,6 +1011,11 @@ def batch_delete_devices(data: Union[list, dict], *, ui_only: bool = False, yes:
1011
1011
  br(cli.central.unarchive_devices, (inv_del_serials,)),
1012
1012
  ]
1013
1013
 
1014
+ # cop only delete devices from GreenLake inventory
1015
+ cop_del_reqs = [] if not inv_del_serials or not config.is_cop else [
1016
+ br(cli.central.cop_delete_device_from_inventory, inv_del_serials)
1017
+ ]
1018
+
1014
1019
  # build reqs to remove devs from monit views. Down devs now, Up devs delayed to allow time to disc.
1015
1020
  mon_del_reqs, delayed_mon_del_reqs = [], []
1016
1021
  for dev_type, _devs in zip(["ap", "switch", "gateway"], [aps, switches, gws]):
@@ -1034,7 +1039,7 @@ def batch_delete_devices(data: Union[list, dict], *, ui_only: bool = False, yes:
1034
1039
  print("")
1035
1040
 
1036
1041
  # None of the provided devices were found in cache or inventory
1037
- if not [*reqs, *mon_del_reqs, *delayed_mon_del_reqs]:
1042
+ if not [*reqs, *mon_del_reqs, *delayed_mon_del_reqs, *cop_del_reqs]:
1038
1043
  print("Everything is as it should be, nothing to do.")
1039
1044
  raise typer.Exit(0)
1040
1045
 
@@ -1043,7 +1048,7 @@ def batch_delete_devices(data: Union[list, dict], *, ui_only: bool = False, yes:
1043
1048
  if len(cache_devs) > 1:
1044
1049
  _msg += "\n".join([f" {d.summary_text}" for d in cache_devs[1:]])
1045
1050
 
1046
- _total_reqs = len([*reqs, *mon_del_reqs, *delayed_mon_del_reqs]) if not ui_only else len(mon_del_reqs)
1051
+ _total_reqs = len([*reqs, *cop_del_reqs, *mon_del_reqs, *delayed_mon_del_reqs]) if not ui_only else len(mon_del_reqs)
1047
1052
 
1048
1053
  if ui_only:
1049
1054
  if delayed_mon_del_reqs:
@@ -1066,7 +1071,7 @@ def batch_delete_devices(data: Union[list, dict], *, ui_only: bool = False, yes:
1066
1071
  console.print("[italic]Cache has not been updated, [cyan]cencli show all -v[/ cyan] will result in a full cache update.[/ italic]")
1067
1072
  cli.display_results(batch_resp, exit_on_fail=True, caption="Re-run command to perform remaining actions.")
1068
1073
 
1069
- if not delayed_mon_del_reqs:
1074
+ if not delayed_mon_del_reqs and not cop_del_reqs:
1070
1075
  # if all reqs OK cache is updated by deleting specific items, otherwise it's a full cache refresh
1071
1076
  all_ok = True if all(r.ok for r in batch_resp) else False
1072
1077
 
@@ -1096,33 +1101,47 @@ def batch_delete_devices(data: Union[list, dict], *, ui_only: bool = False, yes:
1096
1101
  cli.display_results(batch_resp, tablefmt="action")
1097
1102
  raise typer.Exit(0)
1098
1103
 
1099
- del_resp = []
1100
- del_reqs_try = delayed_mon_del_reqs.copy()
1101
- _delay = 10 if not switches else 30 # switches take longer to drop off
1102
- for _try in range(4):
1103
- _word = "more " if _try > 0 else ""
1104
- _prefix = "" if _try == 0 else f"\[Attempt {_try + 1}] "
1105
- _delay -= (5 * _try) # reduce delay by 5 secs for each request
1106
- for _ in track(range(_delay), description=f"{_prefix}[green]Allowing {_word}time for devices to disconnect."):
1107
- sleep(1)
1108
-
1109
- _del_resp = cli.central.batch_request(del_reqs_try, continue_on_fail=True)
1110
- if _try == 3:
1111
- if not all([r.ok for r in _del_resp]):
1112
- print("\n[dark_orange]:warning:[/] Retries exceeded. Devices still remain Up in central and cannot be deleted. This command can be re-ran once they have disconnected.")
1113
- del_resp += _del_resp
1114
- else:
1115
- del_resp += [r for r in _del_resp if r.ok or isinstance(r.output, dict) and r.output.get("error_code", "") != "0007"]
1104
+ elif delayed_mon_del_reqs:
1105
+ del_resp = []
1106
+ del_reqs_try = delayed_mon_del_reqs.copy()
1107
+ _delay = 10 if not switches else 30 # switches take longer to drop off
1108
+ for _try in range(4):
1109
+ _word = "more " if _try > 0 else ""
1110
+ _prefix = "" if _try == 0 else f"\[Attempt {_try + 1}] "
1111
+ _delay -= (5 * _try) # reduce delay by 5 secs for each request
1112
+ for _ in track(range(_delay), description=f"{_prefix}[green]Allowing {_word}time for devices to disconnect."):
1113
+ sleep(1)
1116
1114
 
1117
- del_reqs_try = [del_reqs_try[idx] for idx, r in enumerate(_del_resp) if not r.ok and isinstance(r.output, dict) and r.output.get("error_code", "") == "0007"]
1118
- if del_reqs_try:
1119
- print(f"{len(del_reqs_try)} out of {len(*mon_del_reqs, *delayed_mon_del_reqs)} device{'s are' if len(del_reqs_try) > 1 else ' is'} still [bright_green]Up[/] in Central")
1120
- else:
1121
- break
1115
+ _del_resp = cli.central.batch_request(del_reqs_try, continue_on_fail=True)
1116
+ if _try == 3:
1117
+ if not all([r.ok for r in _del_resp]):
1118
+ print("\n[dark_orange]:warning:[/] Retries exceeded. Devices still remain Up in central and cannot be deleted. This command can be re-ran once they have disconnected.")
1119
+ del_resp += _del_resp
1120
+ else:
1121
+ del_resp += [r for r in _del_resp if r.ok or isinstance(r.output, dict) and r.output.get("error_code", "") != "0007"]
1122
1122
 
1123
- batch_resp += del_resp or _del_resp
1123
+ del_reqs_try = [del_reqs_try[idx] for idx, r in enumerate(_del_resp) if not r.ok and isinstance(r.output, dict) and r.output.get("error_code", "") == "0007"]
1124
+ if del_reqs_try:
1125
+ print(f"{len(del_reqs_try)} out of {len(*mon_del_reqs, *delayed_mon_del_reqs)} device{'s are' if len(del_reqs_try) > 1 else ' is'} still [bright_green]Up[/] in Central")
1126
+ else:
1127
+ break
1128
+
1129
+ batch_resp += del_resp or _del_resp
1130
+
1131
+ # On COP delete devices from GreenLake inventory (only available on CoP)
1132
+ # TODO test against a cop system
1133
+ # TODO add to cencli delete device ...
1134
+ if cop_del_reqs:
1135
+ cop_del_resp = cli.central.batch_request(cop_del_reqs)
1136
+ if not all(r.ok for r in cop_del_resp):
1137
+ print("[bright_red]Errors occured during CoP GreenLake delete")
1138
+ cli.display_results(cop_del_resp, tablefmt="action")
1139
+ else:
1140
+ # display results (below) with results of previous calls
1141
+ batch_resp += cop_del_resp
1124
1142
 
1125
1143
  # TODO need to update cache after ui-only delete
1144
+ # TODO need to improve logic throughout and update inventory cache
1126
1145
  if batch_resp:
1127
1146
  with console.status("Performing cache updates..."):
1128
1147
  db_updates = []
@@ -1804,7 +1823,7 @@ def move(
1804
1823
  elif not import_file:
1805
1824
  _msg = [
1806
1825
  "Usage: cencli batch move [OPTIONS] WHAT:[devices] IMPORT_FILE",
1807
- "Try 'cencli batch add ?' for help.",
1826
+ "Try 'cencli batch move ?' for help.",
1808
1827
  "",
1809
1828
  "Error: One of 'IMPORT_FILE' or --example should be provided.",
1810
1829
  ]
@@ -1814,6 +1833,109 @@ def move(
1814
1833
  resp = batch_move_devices(import_file, yes=yes, do_group=do_group, do_site=do_site, do_label=do_label)
1815
1834
  cli.display_results(resp, tablefmt="action")
1816
1835
 
1836
+
1837
+ @app.command()
1838
+ def archive(
1839
+ import_file: Path = typer.Argument(None, exists=True, show_default=False,),
1840
+ show_example: bool = typer.Option(False, "--example", help="Show Example import file format.", show_default=False),
1841
+ yes: bool = typer.Option(False, "-Y", "-y", help="Bypass confirmation prompts - Assume Yes"),
1842
+ default: bool = typer.Option(
1843
+ False, "-d", is_flag=True, help="Use default central account", show_default=False,
1844
+ callback=cli.default_callback,
1845
+ ),
1846
+ debug: bool = typer.Option(
1847
+ False, "--debug", envvar="ARUBACLI_DEBUG", help="Enable Additional Debug Logging",
1848
+ ),
1849
+ account: str = typer.Option(
1850
+ "central_info",
1851
+ envvar="ARUBACLI_ACCOUNT",
1852
+ help="The Aruba Central Account to use (must be defined in the config)",
1853
+ ),
1854
+ ) -> None:
1855
+ """Batch archive devices based on import data from file.
1856
+
1857
+ This will archive the devices in GreenLake
1858
+ """
1859
+ if show_example:
1860
+ print(examples.archive)
1861
+ return
1862
+
1863
+ elif not import_file:
1864
+ _msg = [
1865
+ "Usage: cencli batch archive [OPTIONS] WHAT:[devices] IMPORT_FILE",
1866
+ "Try 'cencli batch archive ?' for help.",
1867
+ "",
1868
+ "Error: One of 'IMPORT_FILE' or --example should be provided.",
1869
+ ]
1870
+ print("\n".join(_msg))
1871
+ raise typer.Exit(1)
1872
+ else:
1873
+ data = config.get_file_data(import_file, text_ok=True)
1874
+ if data and isinstance(data, list):
1875
+ if all([isinstance(x, dict) for x in data]):
1876
+ serials = [x.get("serial") or x.get("serial_num") for x in data]
1877
+ elif all(isinstance(x, str) for x in data):
1878
+ serials = data if not data[0].lower().startswith("serial") else data[1:]
1879
+ else:
1880
+ print(f"[bright_red]Error[/] Unexpected data structure returned from {import_file.name}")
1881
+ print("Use [cyan]cencli batch archive --example[/] to see expected format.")
1882
+ raise typer.Exit(1)
1883
+
1884
+ res = cli.central.request(cli.central.archive_devices, (serials,))
1885
+ cli.display_results(res)
1886
+
1887
+
1888
+ @app.command()
1889
+ def unarchive(
1890
+ import_file: Path = typer.Argument(None, exists=True, show_default=False,),
1891
+ show_example: bool = typer.Option(False, "--example", help="Show Example import file format.", show_default=False),
1892
+ yes: bool = typer.Option(False, "-Y", "-y", help="Bypass confirmation prompts - Assume Yes"),
1893
+ default: bool = typer.Option(
1894
+ False, "-d", is_flag=True, help="Use default central account", show_default=False,
1895
+ callback=cli.default_callback,
1896
+ ),
1897
+ debug: bool = typer.Option(
1898
+ False, "--debug", envvar="ARUBACLI_DEBUG", help="Enable Additional Debug Logging",
1899
+ ),
1900
+ account: str = typer.Option(
1901
+ "central_info",
1902
+ envvar="ARUBACLI_ACCOUNT",
1903
+ help="The Aruba Central Account to use (must be defined in the config)",
1904
+ ),
1905
+ ) -> None:
1906
+ """Batch unarchive devices based on import data from file.
1907
+
1908
+ This will unarchive the devices (previously archived) in GreenLake
1909
+ """
1910
+ if show_example:
1911
+ print(examples.unarchive)
1912
+ return
1913
+
1914
+ elif not import_file:
1915
+ _msg = [
1916
+ "Usage: cencli batch unarchive [OPTIONS] WHAT:[devices] IMPORT_FILE",
1917
+ "Try 'cencli batch unarchive ?' for help.",
1918
+ "",
1919
+ "Error: One of 'IMPORT_FILE' or --example should be provided.",
1920
+ ]
1921
+ print("\n".join(_msg))
1922
+ raise typer.Exit(1)
1923
+ else:
1924
+ data = config.get_file_data(import_file, text_ok=True)
1925
+ if data and isinstance(data, list):
1926
+ if all([isinstance(x, dict) for x in data]):
1927
+ serials = [x.get("serial") or x.get("serial_num") for x in data]
1928
+ elif all(isinstance(x, str) for x in data):
1929
+ serials = data if not data[0].lower().startswith("serial") else data[1:]
1930
+ else:
1931
+ print(f"[bright_red]Error[/] Unexpected data structure returned from {import_file.name}")
1932
+ print("Use [cyan]cencli batch unarchive --example[/] to see expected format.")
1933
+ raise typer.Exit(1)
1934
+
1935
+ res = cli.central.request(cli.central.unarchive_devices, (serials,))
1936
+ cli.display_results(res)
1937
+
1938
+
1817
1939
  @app.callback()
1818
1940
  def callback():
1819
1941
  """
@@ -580,6 +580,13 @@ def subscription(
580
580
  title = "Subscription Details"
581
581
  _cleaner = cleaner.get_subscriptions
582
582
  set_width_cols = {"name": 40}
583
+ elif what == "auto":
584
+ resp = cli.central.request(cli.central.get_auto_subscribe)
585
+ if resp and "services" in resp.output:
586
+ resp.output = resp.output["services"]
587
+ title = "Services with auto-subscribe enabled"
588
+ _cleaner = None
589
+ set_width_cols = None
583
590
  elif what == "stats":
584
591
  resp = cli.central.request(cli.central.get_subscription_stats)
585
592
  title = "Subscription Stats"
@@ -603,6 +610,7 @@ def subscription(
603
610
  set_width_cols=set_width_cols
604
611
  )
605
612
 
613
+
606
614
  # TODO need sort_by enum
607
615
  @app.command(short_help="Show Swarms (IAP Clusters)")
608
616
  def swarms(
@@ -27,10 +27,10 @@ iden = IdenMetaVars()
27
27
  app = typer.Typer()
28
28
 
29
29
 
30
- @app.command(help="unassign License from device(s)")
30
+ @app.command()
31
31
  def license(
32
32
  license: cli.cache.LicenseTypes = typer.Argument(..., help="License type to unassign from device(s).", show_default=False),
33
- devices: List[str] = typer.Argument(..., metavar=iden.dev_many, autocompletion=cli.cache.dev_completion),
33
+ devices: List[str] = typer.Argument(..., help="device serial numbers or 'auto' to disable auto-subscribe.", metavar=iden.dev_many, autocompletion=cli.cache.dev_completion),
34
34
  yes: bool = typer.Option(False, "-Y", "-y", help="Bypass confirmation prompts - Assume Yes"),
35
35
  debug: bool = typer.Option(False, "--debug", envvar="ARUBACLI_DEBUG", help="Enable Additional Debug Logging",),
36
36
  default: bool = typer.Option(False, "-d", is_flag=True, help="Use default central account", show_default=False,),
@@ -39,6 +39,19 @@ def license(
39
39
  help="The Aruba Central Account to use (must be defined in the config)",
40
40
  autocompletion=cli.cache.account_completion),
41
41
  ) -> None:
42
+ """Unssign Licenses from devices by serial number(s) or disable auto-subscribe for the license type.
43
+ """
44
+ do_auto = True if "auto" in [s.lower() for s in devices] else False
45
+ if do_auto:
46
+ _msg = f"Disable Auto-assignment of [bright_green]{license}[/bright_green] to applicable devices."
47
+ if len(devices) > 1:
48
+ print(f'[cyan]auto[/] keyword provided remaining entries will be [bright_red]ignored[/]')
49
+ print(_msg)
50
+ if yes or typer.confirm("\nProceed?"):
51
+ resp = cli.central.request(cli.central.disable_auto_subscribe, services=license.name)
52
+ cli.display_results(resp, tablefmt="action")
53
+ return
54
+
42
55
  try:
43
56
  devices: CentralObject = [cli.cache.get_dev_identifier(dev) for dev in devices]
44
57
  except typer.Exit: # allows un-assignment of devices that never checked into Central
@@ -279,6 +279,7 @@ class Config:
279
279
  self.debugv: bool = self.data.get("debugv", False)
280
280
  self.sanitize: bool = self.data.get("sanitize", False)
281
281
  self.account = self.get_account_from_args()
282
+ self.base_url = self.data.get(self.account, {}).get("base_url")
282
283
  try:
283
284
  self.webhook = WebHook(**self.data.get(self.account, {}).get("webhook", {}))
284
285
  except ValidationError:
@@ -319,6 +320,10 @@ class Config:
319
320
  else:
320
321
  return self.data.get("central_info", {}).get(item, default)
321
322
 
323
+ @property
324
+ def is_cop(self):
325
+ return False if self.base_url.endswith("arubanetworks.com") else True
326
+
322
327
  # not used but may be handy
323
328
  @property
324
329
  def tokens(self):
@@ -333,6 +333,7 @@ class SubscriptionArgs(str, Enum):
333
333
  details = "details"
334
334
  stats = "stats"
335
335
  names = "names"
336
+ auto = "auto"
336
337
 
337
338
  class ArgToWhat:
338
339
  def __init__(self):
@@ -352,6 +352,8 @@ class ImportExamples:
352
352
  self.deploy = clibatch_deploy
353
353
  self.subscribe = clibatch_subscribe
354
354
  self.unsubscribe = clibatch_unsubscribe
355
+ self.archive = clibatch_unsubscribe.replace("unsubscribe", "archive")
356
+ self.unarchive = clibatch_unsubscribe.replace("unsubscribe", "unarchive")
355
357
  self.move_devices = clibatch_move_devices
356
358
 
357
359
  def __getattr__(self, key: str):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "centralcli"
3
- version = "2.3.0"
3
+ version = "2.3.1"
4
4
  description = "A CLI for interacting with Aruba Central (Cloud Management Platform). Facilitates bulk imports, exports, reporting. A handy tool if you have devices managed by Aruba Central."
5
5
  license = "MIT"
6
6
  authors = ["Wade Wells (Pack3tL0ss) <wade@consolepi.org>"]
File without changes
File without changes
File without changes