centralcli 5.2.2__tar.gz → 5.3.0__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 (58) hide show
  1. {centralcli-5.2.2 → centralcli-5.3.0}/PKG-INFO +1 -1
  2. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cache.py +1 -1
  3. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/central.py +3 -3
  4. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cleaner.py +15 -18
  5. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clibatch.py +14 -3
  6. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clicommon.py +4 -2
  7. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishow.py +31 -27
  8. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowaudit.py +5 -4
  9. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowospf.py +24 -37
  10. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowoverlay.py +12 -13
  11. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliupgrade.py +5 -3
  12. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/constants.py +1 -0
  13. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/render.py +2 -2
  14. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/response.py +36 -18
  15. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/strings.py +0 -15
  16. {centralcli-5.2.2 → centralcli-5.3.0}/pyproject.toml +1 -1
  17. {centralcli-5.2.2 → centralcli-5.3.0}/LICENSE +0 -0
  18. {centralcli-5.2.2 → centralcli-5.3.0}/README.md +0 -0
  19. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/__init__.py +0 -0
  20. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/boilerplate/README.md +0 -0
  21. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/boilerplate/allcalls.py +0 -0
  22. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/caas.py +0 -0
  23. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cli.py +0 -0
  24. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliadd.py +0 -0
  25. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliassign.py +0 -0
  26. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clicaas.py +0 -0
  27. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliclone.py +0 -0
  28. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clidel.py +0 -0
  29. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clidelfirmware.py +0 -0
  30. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliexport.py +0 -0
  31. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clikick.py +0 -0
  32. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clioptions.py +0 -0
  33. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clirefresh.py +0 -0
  34. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clirename.py +0 -0
  35. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliset.py +0 -0
  36. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clisetfirmware.py +0 -0
  37. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowbranch.py +0 -0
  38. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowcloudauth.py +0 -0
  39. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowfirmware.py +0 -0
  40. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowmpsk.py +0 -0
  41. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowtshoot.py +0 -0
  42. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowwids.py +0 -0
  43. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clitest.py +0 -0
  44. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clitshoot.py +0 -0
  45. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliunassign.py +0 -0
  46. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliupdate.py +0 -0
  47. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/config.py +0 -0
  48. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/exceptions.py +0 -0
  49. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/logger.py +0 -0
  50. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/models.py +0 -0
  51. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/objects.py +0 -0
  52. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/setup.py +0 -0
  53. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/static/favicon.ico +0 -0
  54. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/utils.py +0 -0
  55. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/vscodeargs.py +0 -0
  56. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/wh2snow.py +0 -0
  57. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/wh_proxy.py +0 -0
  58. {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/wh_proxy_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: centralcli
3
- Version: 5.2.2
3
+ Version: 5.3.0
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
@@ -1086,7 +1086,7 @@ class Cache:
1086
1086
  err_console.print(":warning: Invalid config")
1087
1087
  return
1088
1088
 
1089
- match = self.get_dev_identifier(incomplete, device_type="gw", completion=True)
1089
+ match = self.get_dev_identifier(incomplete, dev_type="gw", completion=True)
1090
1090
 
1091
1091
  out = []
1092
1092
  if match:
@@ -4041,7 +4041,7 @@ class CentralApi(Session):
4041
4041
  self,
4042
4042
  serial_num: str,
4043
4043
  api: str = "V1",
4044
- offset: int = 0,
4044
+ marker: str = None,
4045
4045
  limit: int = 100
4046
4046
  ) -> Response:
4047
4047
  """Get routes for a device.
@@ -4049,7 +4049,7 @@ class CentralApi(Session):
4049
4049
  Args:
4050
4050
  serial_num (str): Device serial number
4051
4051
  api (str, optional): API version (V0|V1), Defaults to V1.
4052
- offset (str, optional): Pagination offset.
4052
+ marker (str, optional): Pagination offset.
4053
4053
  limit (int, optional): page size Defaults to 100.
4054
4054
 
4055
4055
  Returns:
@@ -4060,7 +4060,7 @@ class CentralApi(Session):
4060
4060
  params = {
4061
4061
  'device': serial_num,
4062
4062
  'api': api,
4063
- 'marker': offset,
4063
+ 'marker': marker,
4064
4064
  'limit': limit
4065
4065
  }
4066
4066
 
@@ -35,6 +35,7 @@ from centralcli.constants import DevTypes, StatusOptions, LLDPCapabilityTypes
35
35
  from centralcli.objects import DateTime
36
36
  from centralcli.models import CloudAuthUploadResponse
37
37
 
38
+ TableFormat = Literal["json", "yaml", "csv", "rich", "tabulate"]
38
39
 
39
40
  def epoch_convert(func):
40
41
  @functools.wraps(func)
@@ -1359,16 +1360,15 @@ def get_client_roaming_history(data: List[dict]) -> List[dict]:
1359
1360
 
1360
1361
  return data
1361
1362
 
1362
- def get_fw_version_list(data: List[dict], format: str = "rich") -> List[dict]:
1363
+ def get_fw_version_list(data: List[dict], format: TableFormat = "rich") -> List[dict]:
1364
+ # Override default behavior of k, v formatter (default which implies rich will use unicode check mark for beta)
1365
+ if format != "rich" and "release_status" in data[-1].keys():
1366
+ _short_value["release_status"] = lambda v: "True" if "beta" in v.lower() else "False"
1367
+
1363
1368
  data = [
1364
1369
  dict(short_value(k, d[k]) for k in d) for d in data
1365
1370
  ]
1366
1371
  data = strip_no_value(data)
1367
- if format != "rich" and data and "beta" in data[-1].keys():
1368
- data = [
1369
- {k: v.replace("\u2705", "True") for k, v in d.items()}
1370
- for d in data
1371
- ]
1372
1372
 
1373
1373
  return data
1374
1374
 
@@ -1590,7 +1590,7 @@ def show_ts_commands(data: Union[List[dict], dict],) -> Union[List[dict], dict]:
1590
1590
 
1591
1591
  return data
1592
1592
 
1593
- def get_overlay_routes(data: Union[List[dict], dict], format: str = "rich", simplify: bool = True) -> Union[List[dict], dict]:
1593
+ def get_overlay_routes(data: Union[List[dict], dict], format: TableFormat = "rich", simplify: bool = True) -> Union[List[dict], dict]:
1594
1594
  if "routes" in data:
1595
1595
  data = data["routes"]
1596
1596
 
@@ -1624,7 +1624,6 @@ def get_overlay_routes(data: Union[List[dict], dict], format: str = "rich", simp
1624
1624
  route = {**base, **route}
1625
1625
  else:
1626
1626
  route = {**{k: "" for k in base.keys()}, **route}
1627
- # outdata += [route] # Put fields in desired order
1628
1627
  outdata += [{k: route.get(k) for k in field_order if k in route.keys()}] # Put fields in desired order
1629
1628
 
1630
1629
  if format == "rich" and data and "is_best" in outdata[-1].keys():
@@ -1633,16 +1632,8 @@ def get_overlay_routes(data: Union[List[dict], dict], format: str = "rich", simp
1633
1632
  for d in outdata
1634
1633
  ]
1635
1634
 
1636
- _short_value["0.0.0.0"] = "0.0.0.0" # OVERRIDE default
1635
+ _short_value["0.0.0.0"] = "0.0.0.0" # OVERRIDE default referenced by simple_kv_formatter. Otherwise will replace with "-"
1637
1636
  data = simple_kv_formatter(outdata)
1638
- # data = simple_kv_formatter(
1639
- # [
1640
- # {
1641
- # **{k: v for k, v in r.items() if k != "nexthop"},
1642
- # "nexthop": [{k: v if k != "interface" else utils.unlistify(v) for k, v in hops.items()} for hops in r["nexthop"]]
1643
- # } for r in data
1644
- # ]
1645
- # )
1646
1637
 
1647
1638
  return data
1648
1639
 
@@ -1660,12 +1651,13 @@ def get_overlay_interfaces(data: Union[List[dict], dict]) -> Union[List[dict], d
1660
1651
 
1661
1652
  return simple_kv_formatter(data)
1662
1653
 
1663
- def get_full_wlan_list(data: List[dict] | str | Dict, verbosity: int = 0) -> List[dict]:
1654
+ def get_full_wlan_list(data: List[dict] | str | Dict, verbosity: int = 0, format: TableFormat = "rich") -> List[dict]:
1664
1655
  if isinstance(data, list) and data and isinstance(data[0], str):
1665
1656
  data = json.loads(data[0])
1666
1657
  if isinstance(data, dict) and "wlans" in data:
1667
1658
  data = data["wlans"]
1668
1659
 
1660
+ # TODO PlaceHolder logic, currently only support verbosity level 0
1669
1661
  verbosity_keys = {
1670
1662
  0: [
1671
1663
  'group',
@@ -1683,6 +1675,7 @@ def get_full_wlan_list(data: List[dict] | str | Dict, verbosity: int = 0) -> Lis
1683
1675
  ]
1684
1676
  }
1685
1677
  pretty_data = []
1678
+
1686
1679
  # rf_band all is a legacy key so all means 2.4 and 5, this updates so that all is only the value if 6 is also enabled.
1687
1680
  # also grabs values for keys that are stored in dicts
1688
1681
  def _simplify_value(wlan: dict, k: str, v: Any) -> Any:
@@ -1704,6 +1697,10 @@ def get_full_wlan_list(data: List[dict] | str | Dict, verbosity: int = 0) -> Lis
1704
1697
  ssid_data["name"] = None
1705
1698
  pretty_data += [ssid_data]
1706
1699
 
1700
+ # override default which swaps in unicode checkmark/X (for rich output)
1701
+ if format != "rich" and "disable_ssid" in data[-1].keys():
1702
+ _short_value["disable_ssid"] = lambda v: 'True' if not v else 'False'
1703
+
1707
1704
  pretty_data = simple_kv_formatter(pretty_data)
1708
1705
  pretty_data = strip_no_value(pretty_data)
1709
1706
  return pretty_data
@@ -1429,11 +1429,12 @@ def batch_delete_groups_or_labels(data: Union[list, dict], *, yes: bool = False,
1429
1429
 
1430
1430
  # FIXME The Loop logic keeps trying if a delete fails despite the device being offline, validate the error check logic
1431
1431
  # TODO batch delete sites does a call for each site, not multi-site endpoint?
1432
- @app.command(short_help="Delete devices.", help=help_text.batch_delete_devices)
1432
+ # TODO make sub-command clibatchdelete.py seperate out sites devices...
1433
+ @app.command(short_help="Delete devices.",)
1433
1434
  def delete(
1434
1435
  what: BatchDelArgs = typer.Argument(..., show_default=False,),
1435
1436
  import_file: Path = typer.Argument(None, exists=True, readable=True, show_default=False, autocompletion=lambda incomplete: [],),
1436
- ui_only: bool = typer.Option(False, "--ui-only", help="Only delete device from UI/Monitoring views. Devices remains assigned and licensed. Devices must be offline."),
1437
+ ui_only: bool = typer.Option(False, "--ui-only", help="Only delete device from UI/Monitoring views (devices must be offline). Devices will remain in inventory with subscriptions unchanged."),
1437
1438
  cop_inv_only: bool = typer.Option(False, "--cop-only", help="Only delete device from CoP inventory.", hidden=True),
1438
1439
  dry_run: bool = typer.Option(False, "--dry-run", help="Testing/Debug Option", hidden=True),
1439
1440
  force: bool = typer.Option(False, "-F", "--force", help="Perform API calls based on input file without validating current states (valid for devices)"),
@@ -1456,7 +1457,17 @@ def delete(
1456
1457
  help="The Aruba Central Account to use (must be defined in the config)",
1457
1458
  ),
1458
1459
  ) -> None:
1459
- """Batch delete Aruba Central Objects [devices|sites|groups|labels] based on input from file.
1460
+ """[bright_green]Perform batch Delete operations using import data from file.[/]
1461
+
1462
+ [cyan]cencli delete sites <IMPORT_FILE>[/] and
1463
+ [cyan]cencli delte groups <IMPORT_FILE>[/]
1464
+ Do what you'd expect.
1465
+
1466
+ [cyan]cencli batch delete devices <IMPORT_FILE>[/]
1467
+
1468
+ Delete devices will remove any subscriptions/licenses from the device and disassociate the device with the Aruba Central app in GreenLake. It will then remove the device from the monitoring views, along with the historical data for the device.
1469
+
1470
+ Note: devices can only be removed from monitoring views if they are in a down state. This command will delay/wait for any Up devices to go Down after the subscriptions/assignment to Central is removed, but it can also be ran again. It will pick up where it left off, skipping any steps that have already been performed.
1460
1471
  """
1461
1472
  if show_example:
1462
1473
  print(getattr(examples, f"delete_{what}"))
@@ -453,7 +453,7 @@ class CLICommon:
453
453
  data: Union[List[dict], List[str], dict, None] = None,
454
454
  tablefmt: TableFormat = "rich",
455
455
  title: str = None,
456
- caption: str = None,
456
+ caption: str | List[str] = None,
457
457
  pager: bool = False,
458
458
  outfile: Path = None,
459
459
  sort_by: str = None,
@@ -481,7 +481,7 @@ class CLICommon:
481
481
  clean bypasses all formatters.
482
482
  title: (str, optional): Title of output table.
483
483
  Only applies to "rich" tablefmt. Defaults to None.
484
- caption: (str, optional): Caption displayed at bottom of table.
484
+ caption: (str | List[str], optional): Caption displayed at bottom of table.
485
485
  Only applies to "rich" tablefmt. Defaults to None.
486
486
  pager (bool, optional): Page Output / or not. Defaults to True.
487
487
  outfile (Path, optional): path/file of output file. Defaults to None.
@@ -497,6 +497,8 @@ class CLICommon:
497
497
  fold_cols (Union[List[str], str], optional): columns that will be folded (wrapped within the same column). Applies to tablfmt=rich. Defaults to [].
498
498
  cleaner (callable, optional): The Cleaner function to use.
499
499
  """
500
+ if isinstance(caption, list):
501
+ caption = "\n ".join(caption)
500
502
  if resp is not None:
501
503
  resp = utils.listify(resp)
502
504
 
@@ -937,7 +937,7 @@ def interfaces(
937
937
  try:
938
938
  up = len([i for i in resp.output if i.get("status").lower() == "up"])
939
939
  down = len(resp.output) - up
940
- caption += [f" Counts: Total: [cyan]{len(resp.output)}[/], Up: [bright_green]{up}[/], Down: [bright_red]{down}[/]"]
940
+ caption += [f"Counts: Total: [cyan]{len(resp.output)}[/], Up: [bright_green]{up}[/], Down: [bright_red]{down}[/]"]
941
941
  except Exception as e:
942
942
  log.error(f"{e.__class__.__name__} while trying to get counts from {dev.name} interface output")
943
943
 
@@ -946,7 +946,7 @@ def interfaces(
946
946
  resp,
947
947
  tablefmt=tablefmt,
948
948
  title=title,
949
- caption="\n".join(caption),
949
+ caption=caption,
950
950
  pager=pager,
951
951
  outfile=outfile,
952
952
  sort_by=sort_by,
@@ -990,8 +990,9 @@ def poe(
990
990
  caption = " Power values are in watts."
991
991
  if resp:
992
992
  resp.output = utils.listify(resp.output) # if they specify an interface output will be a single dict.
993
- _delivering_count = len(list(filter(lambda i: i.get("poe_detection_status", 99) == 3, resp.output)))
994
- caption = f"{caption} Interfaces delivering power: [bright_green]{_delivering_count}[/]"
993
+ if not port:
994
+ _delivering_count = len(list(filter(lambda i: i.get("poe_detection_status", 99) == 3, resp.output)))
995
+ caption = f"{caption} Interfaces delivering power: [bright_green]{_delivering_count}[/]"
995
996
  if "poe_slots" in resp.output[0] and resp.output[0]["poe_slots"]: # CX has the key but it appears to always be an empty dict
996
997
  caption = f"{caption}\n Switch Poe Capabilities (watts): Max: [cyan]{resp.output[0]['poe_slots'].get('maximum_power_in_watts', '?')}[/]"
997
998
  caption = f"{caption}, Draw: [cyan]{resp.output[0]['poe_slots'].get('power_drawn_in_watts', '?')}[/]"
@@ -1159,12 +1160,18 @@ def dhcp(
1159
1160
 
1160
1161
  @app.command(short_help="Show firmware upgrade status")
1161
1162
  def upgrade(
1162
- device: List[str] = typer.Argument(..., metavar=iden_meta.dev, hidden=False, autocompletion=cli.cache.dev_completion),
1163
+ device: List[str] = typer.Argument(
1164
+ ...,
1165
+ metavar=iden_meta.dev,
1166
+ hidden=False,
1167
+ autocompletion=cli.cache.dev_completion,
1168
+ show_default=False,
1169
+ ),
1163
1170
  do_json: bool = typer.Option(False, "--json", is_flag=True, help="Output in JSON"),
1164
1171
  do_yaml: bool = typer.Option(False, "--yaml", is_flag=True, help="Output in YAML"),
1165
1172
  do_csv: bool = typer.Option(False, "--csv", is_flag=True, help="Output in CSV"),
1166
1173
  do_table: bool = typer.Option(False, "--table", help="Output in table format",),
1167
- outfile: Path = typer.Option(None, "--out", help="Output to file (and terminal)", writable=True),
1174
+ outfile: Path = typer.Option(None, "--out", help="Output to file (and terminal)", writable=True, show_default=False),
1168
1175
  pager: bool = typer.Option(False, "--pager", help="Enable Paged Output"),
1169
1176
  update_cache: bool = typer.Option(False, "-U", hidden=True), # Force Update of cli.cache for testing
1170
1177
  default: bool = typer.Option(False, "-d", is_flag=True, help="Use default central account", show_default=False,),
@@ -1571,7 +1578,7 @@ def lldp(
1571
1578
  Valid on APs and CX switches
1572
1579
 
1573
1580
  Use [cyan]cencli show aps -n --site <SITE>[/] to see lldp neighbors for all APs in a site.
1574
- NOTE: AOS-SW will return LLDP neighbors, but only it reports neighbors for connected Aruba devices managed in Central
1581
+ NOTE: AOS-SW will return LLDP neighbors, but only reports neighbors for connected Aruba devices managed in Central
1575
1582
  """
1576
1583
  central = cli.central
1577
1584
 
@@ -1896,6 +1903,7 @@ def token(
1896
1903
 
1897
1904
 
1898
1905
  # TODO clean up output ... single line output
1906
+ # TODO restrict to GWs appears to only work on GW
1899
1907
  @app.command(short_help="Show device routing table")
1900
1908
  def routes(
1901
1909
  device: List[str] = typer.Argument(..., metavar=iden_meta.dev, autocompletion=cli.cache.dev_completion, show_default=False,),
@@ -1924,16 +1932,14 @@ def routes(
1924
1932
 
1925
1933
  tablefmt = cli.get_format(do_json=do_json, do_yaml=do_yaml, do_csv=do_csv, do_table=do_table, default="rich")
1926
1934
  resp = central.request(central.get_device_ip_routes, device.serial)
1927
- if "summary" in resp.output:
1928
- s = resp.summary
1935
+ caption = ""
1936
+ if "summary" in resp.raw:
1937
+ s = resp.raw["summary"]
1929
1938
  caption = (
1930
1939
  f'max: {s.get("maximum")} total: {s.get("total")} default: {s.get("default")} connected: {s.get("connected")} '
1931
1940
  f'static: {s.get("static")} dynamic: {s.get("dynamic")} overlay: {s.get("overlay")} '
1932
1941
  )
1933
- else:
1934
- caption = ""
1935
- if "routes" in resp.output:
1936
- resp.output = resp.output["routes"]
1942
+
1937
1943
 
1938
1944
  cli.display_results(
1939
1945
  resp,
@@ -2008,11 +2014,11 @@ def wlans(
2008
2014
  "calculate_client_count": True,
2009
2015
  }
2010
2016
 
2011
- # TODO only verbosity 0 currently if group is specified
2012
2017
  tablefmt = cli.get_format(do_json=do_json, do_yaml=do_yaml, do_csv=do_csv, do_table=do_table, default="rich")
2013
- if group:
2014
- resp = central.request(central.get_full_wlan_list, group)
2015
- cli.display_results(resp, sort_by=sort_by, reverse=reverse, tablefmt=tablefmt, title=title, pager=pager, outfile=outfile, cleaner=cleaner.get_full_wlan_list, verbosity=verbose)
2018
+ if group: # Specifying the group implies verbose (same # of API calls either way.)
2019
+ resp = central.request(central.get_full_wlan_list, group) # TODO have get_full_wlan_list covert to list of dicts
2020
+ caption = None # if not resp else f"[green]{len(resp.output)}[/] SSIDs configured in group [cyan]{group}[/]" # It's a str need JSON.loads...
2021
+ cli.display_results(resp, title=title, caption=caption, pager=pager, outfile=outfile, sort_by=sort_by, reverse=reverse, tablefmt=tablefmt, cleaner=cleaner.get_full_wlan_list, verbosity=verbose, format=tablefmt)
2016
2022
  elif verbose:
2017
2023
  import json
2018
2024
  group_res = central.request(central.get_groups_properties)
@@ -2039,10 +2045,14 @@ def wlans(
2039
2045
  else:
2040
2046
  resp = group_res
2041
2047
 
2042
- cli.display_results(resp, sort_by=sort_by, reverse=reverse, tablefmt=tablefmt, title=title, pager=pager, outfile=outfile, cleaner=cleaner.get_full_wlan_list, verbosity=0)
2048
+ cli.display_results(resp, sort_by=sort_by, reverse=reverse, tablefmt=tablefmt, title=title, pager=pager, outfile=outfile, cleaner=cleaner.get_full_wlan_list, verbosity=verbose, format=tablefmt)
2043
2049
  else:
2044
2050
  resp = central.request(central.get_wlans, **params)
2045
- cli.display_results(resp, sort_by=sort_by, reverse=reverse, tablefmt=tablefmt, title=title, pager=pager, outfile=outfile, cleaner=cleaner.get_wlans)
2051
+ caption = None
2052
+ if resp:
2053
+ caption = [f'[green]{len(resp.output)}[/] SSIDs, [green]{sum([wlan.get("client_count", 0) for wlan in resp.output])}[/] Wireless Clients.']
2054
+ caption += ["Summary Output, Specify the group ([cyan]--group GROUP[/])", "or use the verbose flag ([cyan]`-v`[/]) for additional details"]
2055
+ cli.display_results(resp, tablefmt=tablefmt, title=title, caption=caption, pager=pager, outfile=outfile, sort_by=sort_by, reverse=reverse, cleaner=cleaner.get_wlans)
2046
2056
 
2047
2057
 
2048
2058
  @app.command()
@@ -2141,12 +2151,6 @@ def clients(
2141
2151
  sort_by: SortClientOptions = typer.Option(None, "--sort", show_default=False, rich_help_panel="Formatting",),
2142
2152
  reverse: bool = typer.Option(False, "-r", help="Reverse output order", show_default=False, rich_help_panel="Formatting",),
2143
2153
  verbose: bool = typer.Option(False, "-v", help="additional details (vertically)", show_default=False, rich_help_panel="Formatting",),
2144
- verbose2: bool = typer.Option(
2145
- False,
2146
- "-vv",
2147
- help="Show raw response (no formatting but still honors --yaml, --csv ... if provided)",
2148
- show_default=False,
2149
- ),
2150
2154
  pager: bool = typer.Option(False, "--pager", help="Enable Paged Output",),
2151
2155
  default: bool = typer.Option(
2152
2156
  False, "-d",
@@ -2259,7 +2263,7 @@ def clients(
2259
2263
  tablefmt = cli.get_format(do_json, do_yaml, do_csv, do_table, default="rich" if not verbose else "yaml")
2260
2264
 
2261
2265
  verbose_kwargs = {}
2262
- if not verbose2 and not denylisted:
2266
+ if not denylisted:
2263
2267
  verbose_kwargs["cleaner"] = cleaner.get_clients
2264
2268
  verbose_kwargs["cache"] = cli.cache
2265
2269
  verbose_kwargs["verbose"] = verbose
@@ -2284,7 +2288,7 @@ def clients(
2284
2288
  **verbose_kwargs
2285
2289
  )
2286
2290
 
2287
- # TODO Sortby Enum
2291
+
2288
2292
  @app.command()
2289
2293
  def tunnels(
2290
2294
  gateway: str = typer.Argument(..., metavar=iden_meta.dev, autocompletion=cli.cache.dev_gw_completion, case_sensitive=False, show_default=False,),
@@ -46,12 +46,13 @@ def show_logs_cencli_callback(ctx: typer.Context, cencli: bool):
46
46
  return cencli
47
47
 
48
48
 
49
+ # TODO remove verbose2 using --raw now
49
50
  @app.command(
50
51
  help="Show Audit Logs. Audit Logs will displays prior 5 days if no time options are provided.",
51
52
  short_help="Show Audit Logs (last 5 days default)",
52
53
  )
53
54
  def logs(
54
- args: List[str] = typer.Argument(
55
+ log_id: str = typer.Argument(
55
56
  None,
56
57
  metavar='[LOG_ID]',
57
58
  help="Show details for a specific log_id",
@@ -111,13 +112,13 @@ def logs(
111
112
  autocompletion=cli.cache.account_completion,
112
113
  ),
113
114
  ) -> None:
114
- if cencli or (args and args[-1] == "cencli"):
115
+ if cencli or (log_id and log_id[-1] == "cencli"):
115
116
  from centralcli import log
116
117
  log.print_file() if not tail else log.follow()
117
118
  raise typer.Exit(0)
118
119
 
119
- if args:
120
- log_id = cli.cache.get_log_identifier(args[-1])
120
+ if log_id:
121
+ log_id = cli.cache.get_log_identifier(log_id)
121
122
  else:
122
123
  log_id = None
123
124
  if device:
@@ -57,7 +57,7 @@ def neighbors(
57
57
  dev = cli.cache.get_dev_identifier(device)
58
58
  resp = central.request(central.get_ospf_neighbor, dev.serial)
59
59
  tablefmt = cli.get_format(do_json, do_yaml, do_csv, do_table, default="rich" if not verbose else "yaml")
60
- cf = "[reset][italic dark_olive_green2]"
60
+ # cf = "[reset][italic dark_olive_green2]"
61
61
  caption = ""
62
62
 
63
63
  if resp.raw.get("summary"):
@@ -67,23 +67,20 @@ def neighbors(
67
67
  raise typer.Exit(0)
68
68
  else:
69
69
  caption = [
70
- f'{cf} Router ID:[/] {summary["router_id"]} | {cf}OSPF Neigbors:[/] {summary["neighbor_count"]} | {cf}OSPF Interfaces:[/] {summary["interface_count"]}',
71
- f'{cf}OSPF Areas:[/] {summary["area_count"]} | {cf}active LSA:[/] {summary["active_lsa_count"]} | {cf}rexmt LSA:[/] {summary["rexmt_lsa_count"]}'
70
+ f'[cyan]Router ID[/]: {summary["router_id"]} | [cyan]OSPF Neigbors[/]: {summary["neighbor_count"]} | [cyan]OSPF Interfaces[/]: {summary["interface_count"]}',
71
+ f'[cyan]OSPF Areas[/]: {summary["area_count"]} | [cyan]active LSA[/]: {summary["active_lsa_count"]} | [cyan]rexmt LSA[/]: {summary["rexmt_lsa_count"]}'
72
72
  ]
73
- caption = "\n ".join(caption)
74
73
 
75
- title = f"{dev.name} OSPF Neighbors"
76
-
77
- cli.display_results(\
74
+ cli.display_results(
78
75
  resp,
79
76
  tablefmt=tablefmt,
80
- title=title,
77
+ title=f"{dev.name} OSPF Neighbors",
81
78
  pager=pager,
82
79
  outfile=outfile,
83
80
  sort_by=sort_by,
84
81
  reverse=reverse,
85
82
  cleaner=cleaner.get_ospf_neighbor if not verbose else None,
86
- caption=f"{caption}\n"
83
+ caption=caption
87
84
  )
88
85
 
89
86
 
@@ -120,33 +117,29 @@ def interfaces(
120
117
  dev = cli.cache.get_dev_identifier(device)
121
118
  resp = central.request(central.get_ospf_interface, dev.serial)
122
119
  tablefmt = cli.get_format(do_json, do_yaml, do_csv, do_table, default="rich" if not verbose else "yaml")
123
- cf = "[reset][italic dark_olive_green2]"
120
+ # cf = "[reset][italic dark_olive_green2]"
124
121
  caption = ""
125
122
 
126
123
  if resp.raw.get("summary"):
127
124
  summary = resp.raw["summary"]
128
125
  if summary["admin_status"] is False:
129
- print(f"OSPF is not enabled on {dev.name}")
130
- raise typer.Exit(0)
126
+ cli.exit(f"OSPF is not enabled on {dev.name}", code=0)
131
127
  else:
132
128
  caption = [
133
- f'{cf} Router ID:[/] {summary["router_id"]} | {cf}OSPF Neigbors:[/] {summary["neighbor_count"]} | {cf}OSPF Interfaces:[/] {summary["interface_count"]}',
134
- f'{cf}OSPF Areas:[/] {summary["area_count"]} | {cf}active LSA:[/] {summary["active_lsa_count"]} | {cf}rexmt LSA:[/] {summary["rexmt_lsa_count"]}'
129
+ f'[cyan]Router ID[/]: {summary["router_id"]} | [cyan]OSPF Neigbors[/]: {summary["neighbor_count"]} | [cyan]OSPF Interfaces[/]: {summary["interface_count"]}',
130
+ f'[cyan]OSPF Areas[/]: {summary["area_count"]} | [cyan]active LSA[/]: {summary["active_lsa_count"]} | [cyan]rexmt LSA[/]: {summary["rexmt_lsa_count"]}'
135
131
  ]
136
- caption = "\n ".join(caption)
137
-
138
- title = f"{dev.name} OSPF Interfaces"
139
132
 
140
133
  cli.display_results(
141
134
  resp,
142
135
  tablefmt=tablefmt,
143
- title=title,
136
+ title=f"{dev.name} OSPF Interfaces",
144
137
  pager=pager,
145
138
  outfile=outfile,
146
139
  sort_by=sort_by,
147
140
  reverse=reverse,
148
141
  cleaner=cleaner.get_ospf_interface if not verbose else None,
149
- caption=f"{caption}\n",
142
+ caption=caption,
150
143
  full_cols=["ip/mask", "DR IP", "BDR IP", "DR rtr id", "BDR rtr id"]
151
144
  )
152
145
 
@@ -182,33 +175,29 @@ def area(
182
175
  dev = cli.cache.get_dev_identifier(device)
183
176
  resp = central.request(central.get_ospf_area, dev.serial)
184
177
  tablefmt = cli.get_format(do_json, do_yaml, do_csv, do_table, default="rich" if not verbose else "yaml")
185
- cf = "[reset][italic dark_olive_green2]"
178
+ # cf = "[reset][italic dark_olive_green2]"
186
179
  caption = ""
187
180
 
188
181
  if resp.raw.get("summary"):
189
182
  summary = resp.raw["summary"]
190
183
  if summary["admin_status"] is False:
191
- print(f"OSPF is not enabled on {dev.name}")
192
- raise typer.Exit(0)
184
+ cli.exit(f"OSPF is not enabled on {dev.name}", code=0)
193
185
  else:
194
186
  caption = [
195
- f'{cf} Router ID:[/] {summary["router_id"]} | {cf}OSPF Neigbors:[/] {summary["neighbor_count"]} | {cf}OSPF Interfaces:[/] {summary["interface_count"]}',
196
- f'{cf}OSPF Areas:[/] {summary["area_count"]} | {cf}active LSA:[/] {summary["active_lsa_count"]} | {cf}rexmt LSA:[/] {summary["rexmt_lsa_count"]}'
187
+ f'[cyan]Router ID[/]: {summary["router_id"]} | [cyan]OSPF Neigbors[/]: {summary["neighbor_count"]} | [cyan]OSPF Interfaces[/]: {summary["interface_count"]}',
188
+ f'[cyan]OSPF Areas[/]: {summary["area_count"]} | [cyan]active LSA[/]: {summary["active_lsa_count"]} | [cyan]rexmt LSA[/]: {summary["rexmt_lsa_count"]}'
197
189
  ]
198
- caption = "\n ".join(caption)
199
-
200
- title = f"{dev.name} OSPF Area Details"
201
190
 
202
191
  cli.display_results(
203
192
  resp,
204
193
  tablefmt=tablefmt,
205
- title=title,
194
+ title=f"{dev.name} OSPF Area Details",
206
195
  pager=pager,
207
196
  outfile=outfile,
208
197
  sort_by=sort_by,
209
198
  reverse=reverse,
210
199
  cleaner=cleaner.get_ospf_neighbor if not verbose else None, # ospf_neighbor cleaner is sufficient here
211
- caption=f"{caption}\n",
200
+ caption=caption,
212
201
  full_cols=["ip/mask", "DR IP", "BDR IP", "DR rtr id", "BDR rtr id"]
213
202
  )
214
203
 
@@ -244,8 +233,9 @@ def database(
244
233
  dev = cli.cache.get_dev_identifier(device)
245
234
  resp = central.request(central.get_ospf_database, dev.serial)
246
235
  tablefmt = cli.get_format(do_json, do_yaml, do_csv, do_table, default="yaml")
247
- cf = "[reset][italic dark_olive_green2]"
236
+ # cf = "[reset][italic dark_olive_green2]"
248
237
  caption = ""
238
+ pad = " " if tablefmt == "yaml" else ""
249
239
 
250
240
  if resp.raw.get("summary"):
251
241
  summary = resp.raw["summary"]
@@ -254,23 +244,20 @@ def database(
254
244
  raise typer.Exit(0)
255
245
  else:
256
246
  caption = [
257
- f'{cf} Router ID:[/] {summary["router_id"]} | {cf}OSPF Neigbors:[/] {summary["neighbor_count"]} | {cf}OSPF Interfaces:[/] {summary["interface_count"]}',
258
- f'{cf}OSPF Areas:[/] {summary["area_count"]} | {cf}active LSA:[/] {summary["active_lsa_count"]} | {cf}rexmt LSA:[/] {summary["rexmt_lsa_count"]}'
247
+ f'[cyan]{pad}Router ID[/]: {summary["router_id"]} | [cyan]OSPF Neigbors[/]: {summary["neighbor_count"]} | [cyan]OSPF Interfaces[/]: {summary["interface_count"]}',
248
+ f'[cyan]OSPF Areas[/]: {summary["area_count"]} | [cyan]active LSA[/]: {summary["active_lsa_count"]} | [cyan]rexmt LSA[/]: {summary["rexmt_lsa_count"]}'
259
249
  ]
260
- caption = "\n ".join(caption)
261
-
262
- title = f"{dev.name} OSPF Database Details"
263
250
 
264
251
  cli.display_results(
265
252
  resp,
266
253
  tablefmt=tablefmt,
267
- title=title,
254
+ title=f"{dev.name} OSPF Database Details",
268
255
  pager=pager,
269
256
  outfile=outfile,
270
257
  sort_by=sort_by,
271
258
  reverse=reverse,
272
259
  cleaner=cleaner.get_ospf_neighbor if not verbose else None, # ospf_neighbor cleaner is sufficient here
273
- caption=f"{caption}\n",
260
+ caption=caption,
274
261
  full_cols=["ip/mask", "DR IP", "BDR IP", "DR rtr id", "BDR rtr id"]
275
262
  )
276
263
 
@@ -57,7 +57,6 @@ def _build_caption(resp: Response) -> str | None:
57
57
 
58
58
  @app.command()
59
59
  def routes(
60
- # what: OverlayRoutesArgs = typer.Argument("learned", case_sensitive=False, show_default=True),
61
60
  device: str = typer.Argument(..., metavar=iden_meta.dev, autocompletion=cli.cache.dev_ap_gw_completion, show_default=False,),
62
61
  advertised: bool = typer.Option(False, "--advertised", "-a", help="Show advertised routes [grey42]\[default: show learned routes][/]"),
63
62
  best: bool = typer.Option(False, "--best", "-b", help="Return only best/preferred route for each destination"),
@@ -152,11 +151,10 @@ def interfaces(
152
151
  )
153
152
 
154
153
 
154
+ # single entry output, no need to sort
155
155
  @app.command()
156
156
  def connection(
157
- device: str = typer.Argument(..., metavar=iden_meta.dev, autocompletion=cli.cache.dev_ap_gw_completion, show_default=False,),
158
- # sort_by: str = typer.Option(None, "--sort", help="Field to sort by", rich_help_panel="Formatting", show_default=False,), # single entry output, no need to sort
159
- # reverse: bool = typer.Option(False, "-r", is_flag=True, help="Sort in descending order", rich_help_panel="Formatting"),
157
+ device: str = typer.Argument(..., metavar=iden_meta.dev, autocompletion=cli.cache.dev_gw_completion, show_default=False,),
160
158
  do_json: bool = typer.Option(False, "--json", is_flag=True, help="Output in JSON", rich_help_panel="Formatting"),
161
159
  do_yaml: bool = typer.Option(False, "--yaml", is_flag=True, help="Output in YAML", rich_help_panel="Formatting"),
162
160
  do_csv: bool = typer.Option(False, "--csv", is_flag=True, help="Output in CSV", rich_help_panel="Formatting"),
@@ -173,25 +171,28 @@ def connection(
173
171
  rich_help_panel="Common Options",
174
172
  ),
175
173
  ):
176
- """Show overlay connection (OTO/ORO) details (GWs & APs)
174
+ """Show overlay connection (OTO/ORO) details (Valid on SD-Branch GWs/ VPNCs Only)
177
175
 
178
- In testing this API endpoint always returned an error (request to ce failed... timed out) for APs.
179
- You can get similar details using [cyan]cencli tshoot overlay DEVICE[/].
176
+ For additional details use [cyan]cencli tshoot overlay DEVICE[/] (which also works on APs).
180
177
  """
181
- dev = cli.cache.get_dev_identifier(device, dev_type=("gw", "ap",))
178
+ dev = cli.cache.get_dev_identifier(device, dev_type="gw")
182
179
  tablefmt = cli.get_format(do_json=do_json, do_yaml=do_yaml, do_csv=do_csv, do_table=do_table, default="rich")
183
180
 
184
181
  resp = cli.central.request(cli.central.get_overlay_connection, dev.serial)
185
182
 
186
- set_width_cols = {"name": 60}
183
+ set_width_cols = {}
187
184
  caption = None
188
185
  if "connection" in resp.output:
189
186
  resp.output = resp.output["connection"]
190
187
  caption=_build_caption(resp)
191
- elif "summary" in resp.output:
188
+ elif "summary" in resp.output: # For Mobility GWs this endpoint only shows Overlay for SD-Branch
192
189
  resp.output = resp.output["summary"]
193
190
  if resp.output.get("admin_status") is False:
194
- set_width_cols = {"admin status": {"min": 55, "max": 100}}
191
+ caption = [
192
+ "This command only shows Overlay connection status for SD-Branch/VPNC GWs",
193
+ "Use [cyan]cencli tshoot overlay DEVICE[/] for Mobility GWs/APs"
194
+ ]
195
+ set_width_cols = {"admin status": {"min": 72, "max": 100}}
195
196
 
196
197
 
197
198
  cli.display_results(
@@ -201,8 +202,6 @@ def connection(
201
202
  caption=caption,
202
203
  pager=pager,
203
204
  outfile=outfile,
204
- # sort_by=sort_by,
205
- # reverse=reverse,
206
205
  set_width_cols=set_width_cols,
207
206
  cleaner=cleaner.simple_kv_formatter,
208
207
  )
@@ -34,8 +34,9 @@ app = typer.Typer()
34
34
  def device(
35
35
  device: str = typer.Argument(
36
36
  ...,
37
- metavar=iden.dev, show_default=False,
37
+ metavar=iden.dev,
38
38
  autocompletion=cli.cache.dev_completion,
39
+ show_default=False,
39
40
  ),
40
41
  version: str = typer.Argument(
41
42
  None,
@@ -90,6 +91,7 @@ def group(
90
91
  metavar=iden.group,
91
92
  help="Upgrade devices by group",
92
93
  autocompletion=cli.cache.group_completion,
94
+ show_default=False,
93
95
  ),
94
96
  version: str = typer.Argument(
95
97
  None,
@@ -108,8 +110,8 @@ def group(
108
110
  show_default=False,
109
111
  formats=["%m/%d/%Y %H:%M", "%d %H:%M"],
110
112
  ),
111
- dev_type: AllDevTypes = typer.Option(..., help="Upgrade a specific device type",),
112
- model: str = typer.Option(None, help="[applies to switches only] Upgrade a specific switch model"),
113
+ dev_type: AllDevTypes = typer.Option(..., help="Upgrade a specific device type", show_default=False,),
114
+ model: str = typer.Option(None, help="[applies to switches only] Upgrade a specific switch model", show_default=False,),
113
115
  reboot: bool = typer.Option(False, "-R", help="Automatically reboot device after firmware download (APs will reboot regardless)"),
114
116
  yes: bool = typer.Option(False, "-Y", "-y", help="Bypass confirmation prompts - Assume Yes"),
115
117
  debug: bool = typer.Option(False, "--debug", envvar="ARUBACLI_DEBUG", help="Enable Additional Debug Logging",),
@@ -188,6 +188,7 @@ STRIP_KEYS = [
188
188
  "lsas",
189
189
  "commands",
190
190
  "stacks",
191
+ "routes",
191
192
  ]
192
193
 
193
194
 
@@ -426,8 +426,8 @@ def output(
426
426
  formatters.Terminal256Formatter(style='solarized-dark')
427
427
  )
428
428
 
429
- if isinstance(raw_data, str): # HACK
430
- raw_data = raw_data.replace('✅', 'True').replace('❌', 'False') # TODO handle this better messes up column spacing if replacing string.
429
+ if isinstance(raw_data, str): # HACK replace first pass is to line up cols but if table is tighter second pass will swap them regardless
430
+ raw_data = raw_data.replace('✅ ', 'True').replace('❌ ', 'False').replace('✅', 'True').replace('❌', 'False') # TODO handle this better
431
431
 
432
432
  return Output(rawdata=raw_data, prettydata=table_data, config=config)
433
433
 
@@ -160,8 +160,14 @@ class Response:
160
160
  self.status = response.status
161
161
  self.method = response.method
162
162
  _offset_str = ""
163
- if "offset" in response.url.query and int(response.url.query['offset']) > 0:
164
- _offset_str = f" offset: {response.url.query['offset']} limit: {response.url.query.get('limit', '?')}"
163
+ # /routing endpoints use "marker" rather than "offset" for pagination
164
+ offset_key = "marker" if "marker" in response.url.query and ("marker" in response.url.query or response.url.path.startswith("/api/routing/")) else "offset"
165
+ if offset_key in response.url.query:
166
+ if offset_key == "offset" and int(response.url.query[offset_key]) > 0: # only show full query_str if call is beyond first page of results.
167
+ _offset_str = f" {offset_key}: {response.url.query[offset_key]} limit: {response.url.query.get('limit', '?')}"
168
+ else: # marker is not an int
169
+ _offset_str = f" {offset_key}: {response.url.query[offset_key]} limit: {response.url.query.get('limit', '?')}"
170
+
165
171
  _log_msg = f"[{response.reason}] {response.method}:{response.url.path}{_offset_str} Elapsed: {elapsed:.2f}"
166
172
  if not self.ok:
167
173
  self.output = self.output or self.error
@@ -577,6 +583,9 @@ class Session():
577
583
  # TODO cleanup, if we do strip_none here can remove from calling funcs.
578
584
  params = utils.strip_none(params)
579
585
 
586
+ # /routing endpoints use "marker" rather than "offset" for pagination
587
+ offset_key = "marker" if "marker" in params or "/api/routing/" in url else "offset"
588
+
580
589
  # for debugging can set a smaller limit in config or via --debug-limit flag to test paging
581
590
  if params and params.get("limit") and config.limit:
582
591
  log.info(f'paging limit being overridden by config: {params.get("limit")} --> {config.limit}')
@@ -615,14 +624,14 @@ class Session():
615
624
  # On 1st call determine if remaining calls can be made in batch
616
625
  # total is provided for some calls with the total # of records available
617
626
  # TODO no strip_none for these, may need to add if we determine a scenario needs it.
618
- if params.get("offset", 99) == 0 and isinstance(r.raw, dict) and r.raw.get("total") and (len(r.output) + params.get("limit", 0) < r.raw.get("total")):
627
+ if params.get(offset_key, 99) == 0 and isinstance(r.raw, dict) and r.raw.get("total") and (len(r.output) + params.get("limit", 0) < r.raw.get("total")):
619
628
  _total = r.raw["total"] if not url.endswith("/monitoring/v2/events") or r.raw["total"] <= 10_000 else 10_000 # events endpoint will fail if offset + limit > 10,000
620
629
  if _total > len(r.output):
621
630
  _limit = params.get("limit", 100)
622
- _offset = params.get("offset", 0)
631
+ _offset = params.get(offset_key, 0)
623
632
  br = BatchRequest
624
633
  _reqs = [
625
- br(self.exec_api_call, url, data=data, json_data=json_data, method=method, headers=headers, params={**params, "offset": i, "limit": _limit}, **kwargs)
634
+ br(self.exec_api_call, url, data=data, json_data=json_data, method=method, headers=headers, params={**params, offset_key: i, "limit": _limit}, **kwargs)
626
635
  for i in range(len(r.output), _total, _limit)
627
636
  ]
628
637
 
@@ -637,7 +646,7 @@ class Session():
637
646
  elif len(failures) >= len(successful):
638
647
  log.error(f"Failure rate exceeded during batch {method} calls to {url}. Stopping execution.", show=True)
639
648
  elif failures:
640
- log_sfx = "" if len(failures) > 1 else f"?offset={failures[-1].url.query.get('offset')}&limit={failures[-1].url.query.get('limit')}..."
649
+ log_sfx = "" if len(failures) > 1 else f"?{offset_key}={failures[-1].url.query.get(offset_key)}&limit={failures[-1].url.query.get('limit')}..."
641
650
  log.error(f":warning: Output incomplete. {len(failures)} failure occured: [{failures[-1].method}] {failures[-1].url.path}{log_sfx}", caption=True)
642
651
 
643
652
  page_res = [
@@ -648,22 +657,31 @@ class Session():
648
657
  break
649
658
 
650
659
  _limit = params.get("limit", 0)
651
- _offset = params.get("offset", 0)
652
- if params.get("limit") and r.output and len(r.output) == _limit:
653
- if count and len(paged_output) >= count:
660
+ if offset_key == "offset":
661
+ _offset = params.get(offset_key, 0)
662
+ if params.get("limit") and r.output and len(r.output) == _limit:
663
+ if count and len(paged_output) >= count:
664
+ r.output = paged_output
665
+ r.raw = paged_raw
666
+ break
667
+ elif count and len(paged_output) < count:
668
+ next_limit = count - len(paged_output)
669
+ next_limit = _limit if next_limit > _limit else next_limit
670
+ params[offset_key] = _offset + next_limit
671
+ else:
672
+ params[offset_key] = _offset + _limit
673
+ else:
654
674
  r.output = paged_output
655
675
  r.raw = paged_raw
656
676
  break
657
- elif count and len(paged_output) < count:
658
- next_limit = count - len(paged_output)
659
- next_limit = _limit if next_limit > _limit else next_limit
660
- params["offset"] = _offset + next_limit
677
+ else: # The routing api endpoints use an opaque handle representing the next page or results, so they can not be batched, as we need the result to get the marker for the next call
678
+ if r.raw.get("marker"):
679
+ params["marker"] = r.raw["marker"]
661
680
  else:
662
- params["offset"] = _offset + _limit
663
- else:
664
- r.output = paged_output
665
- r.raw = paged_raw
666
- break
681
+ r.raw, r.output = paged_raw, paged_output
682
+ if r.raw.get("marker"):
683
+ del r.raw["marker"]
684
+ break
667
685
 
668
686
  return r
669
687
 
@@ -251,20 +251,6 @@ name\nphl-access\nsan-dc-tor\ncom-branches
251
251
  {common_add_delete_end}
252
252
  """
253
253
 
254
- clibatch_delete_devices_help = """
255
- [bright_green]Perform batch Delete operations using import data from file.[/]
256
-
257
- [cyan]cencli delete sites <IMPORT_FILE>[/] and
258
- [cyan]cencli delte groups <IMPORT_FILE>[/]
259
- Do what you'd expect.
260
-
261
- [cyan]cencli batch delete devices <IMPORT_FILE>[/]
262
-
263
- Delete devices will remove any subscriptions/licenses from the device and disassociate the device with the Aruba Central app in GreenLake. It will then remove the device from the monitoring views, along with the historical data for the device.
264
-
265
- Note: devices can only be removed from monitoring views if they are in a down state. This command will delay/wait for any Up devices to go Down after the subscriptions/assignment to Central is removed, but it can also be ran again. It will pick up where it left off, skipping any steps that have already been performed.
266
- """
267
-
268
254
  _site_common = """
269
255
  [cyan]Provide geo-loc or address details, (Google Maps "Plus Codes" are supported) not both.
270
256
  Can provide both in subsequent calls, but api does not allow both in same call.[reset]
@@ -401,7 +387,6 @@ class ImportExamples:
401
387
 
402
388
  class LongHelp:
403
389
  def __init__(self):
404
- self.batch_delete_devices = do_capture(clibatch_delete_devices_help)
405
390
  self.update_site = do_capture(cliupdate_site_help)
406
391
  self.add_site = do_capture(cliadd_site_help)
407
392
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "centralcli"
3
- version = "5.2.2"
3
+ version = "5.3.0"
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