centralcli 4.0.2__tar.gz → 4.2.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 (57) hide show
  1. {centralcli-4.0.2 → centralcli-4.2.0}/PKG-INFO +1 -1
  2. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/__init__.py +56 -0
  3. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/caas.py +0 -2
  4. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cache.py +85 -29
  5. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/central.py +115 -94
  6. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cleaner.py +80 -14
  7. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cli.py +80 -51
  8. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cliadd.py +7 -27
  9. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clibatch.py +94 -37
  10. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clicommon.py +14 -2
  11. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clidel.py +1 -2
  12. centralcli-4.2.0/centralcli/cliexport.py +60 -0
  13. centralcli-4.2.0/centralcli/clioptions.py +11 -0
  14. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clirename.py +1 -4
  15. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clishow.py +221 -219
  16. centralcli-4.2.0/centralcli/clishowaudit.py +203 -0
  17. centralcli-4.2.0/centralcli/clishowcloudauth.py +95 -0
  18. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clitshoot.py +3 -1
  19. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cliupdate.py +1 -1
  20. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/config.py +6 -4
  21. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/constants.py +6 -0
  22. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/logger.py +1 -1
  23. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/models.py +34 -1
  24. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/response.py +36 -18
  25. {centralcli-4.0.2 → centralcli-4.2.0}/pyproject.toml +1 -1
  26. {centralcli-4.0.2 → centralcli-4.2.0}/LICENSE +0 -0
  27. {centralcli-4.0.2 → centralcli-4.2.0}/README.md +0 -0
  28. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/boilerplate/README.md +0 -0
  29. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/boilerplate/allcalls.py +0 -0
  30. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cliassign.py +0 -0
  31. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clicaas.py +0 -0
  32. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cliclone.py +0 -0
  33. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clidelfirmware.py +0 -0
  34. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clikick.py +0 -0
  35. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clirefresh.py +0 -0
  36. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cliset.py +0 -0
  37. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clisetfirmware.py +0 -0
  38. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clishowbranch.py +0 -0
  39. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clishowfirmware.py +0 -0
  40. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clishowospf.py +0 -0
  41. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clishowoverlay.py +0 -0
  42. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clishowtshoot.py +0 -0
  43. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clishowwids.py +0 -0
  44. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/clitest.py +0 -0
  45. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cliunassign.py +0 -0
  46. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/cliupgrade.py +0 -0
  47. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/exceptions.py +0 -0
  48. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/objects.py +0 -0
  49. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/render.py +0 -0
  50. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/setup.py +0 -0
  51. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/static/favicon.ico +0 -0
  52. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/strings.py +0 -0
  53. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/utils.py +0 -0
  54. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/vscodeargs.py +0 -0
  55. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/wh2snow.py +0 -0
  56. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/wh_proxy.py +0 -0
  57. {centralcli-4.0.2 → centralcli-4.2.0}/centralcli/wh_proxy_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: centralcli
3
- Version: 4.0.2
3
+ Version: 4.2.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
@@ -8,6 +8,7 @@ import os
8
8
  import typer
9
9
 
10
10
  from pathlib import Path
11
+ from typing import Iterable, List
11
12
  import sys
12
13
 
13
14
  import click
@@ -63,6 +64,61 @@ log = MyLogger(log_file, debug=config.debug, show=config.debug, verbose=config.d
63
64
  log.debug(f"{__name__} __init__ calling script: {_calling_script}, base_dir: {config.base_dir}")
64
65
  log.debugv(f"config attributes: {json.dumps({k: str(v) for k, v in config.__dict__.items()}, indent=4)}")
65
66
 
67
+
68
+ # HACK completion has gotten jacked up. typer calls click.utils._expand_args in Windows which was added in click 8, but completion is broken in click 8 so cencli is pinned to 7.1.2 until I can investigate further
69
+ # This hack manually adds the _expand_args functionality to click.utils which is pinned to 7.1.2 Otherwise an exception is thrown on Windows.
70
+ if os.name == "nt": # pragma: no cover
71
+ def _expand_args(
72
+ args: Iterable[str],
73
+ *,
74
+ user: bool = True,
75
+ env: bool = True,
76
+ glob_recursive: bool = True,
77
+ ) -> List[str]:
78
+ """Simulate Unix shell expansion with Python functions.
79
+
80
+ See :func:`glob.glob`, :func:`os.path.expanduser`, and
81
+ :func:`os.path.expandvars`.
82
+
83
+ This is intended for use on Windows, where the shell does not do any
84
+ expansion. It may not exactly match what a Unix shell would do.
85
+
86
+ :param args: List of command line arguments to expand.
87
+ :param user: Expand user home directory.
88
+ :param env: Expand environment variables.
89
+ :param glob_recursive: ``**`` matches directories recursively.
90
+
91
+ :meta private:
92
+ """
93
+ from glob import glob
94
+ import re
95
+
96
+ out = []
97
+
98
+ for arg in args:
99
+ if user:
100
+ arg = os.path.expanduser(arg)
101
+
102
+ if env:
103
+ arg = os.path.expandvars(arg)
104
+
105
+ try:
106
+ matches = glob(arg, recursive=glob_recursive)
107
+ except re.error:
108
+ matches = []
109
+
110
+ if not matches:
111
+ out.append(arg)
112
+ else:
113
+ out.extend(matches)
114
+
115
+ return out
116
+
117
+ import click
118
+ if not hasattr(click.utils, "_expand_args"):
119
+ click.utils._expand_args = _expand_args
120
+
121
+
66
122
  from pycentral.base import ArubaCentralBase
67
123
  from .utils import Utils
68
124
  utils = Utils()
@@ -128,8 +128,6 @@ class BuildCLI:
128
128
  common = self.data[dev]["_common"]
129
129
  vlans = self.data[dev]["vlans"]
130
130
  _pretty_name = typer.style(common.get('hostname', dev), fg="bright_green")
131
- # print(f"Verifying {_pretty_name} is in Group {common['group']}...", end='')
132
- # group_devs = self.session.get_gateways_by_group(self.data[dev]["_common"]["group"])
133
131
  resp = self.central.request(self.central.get_devices, "gateways")
134
132
  gateways = resp.output
135
133
  self.dev_info = [_dev for _dev in gateways if _dev.get('mac', '').lower() == dev.lower()]
@@ -590,7 +590,7 @@ class Cache:
590
590
  incomplete: str,
591
591
  args: List[str] = [],
592
592
  ) -> Generator[Tuple[str, str], None, None] | None:
593
- """Device completion for returning matches that are either switch or AP
593
+ """Device completion for returning matches that are switches (AOS-SW or CX)
594
594
 
595
595
  Args:
596
596
  incomplete (str): The last partial or full command before completion invoked.
@@ -617,6 +617,40 @@ class Cache:
617
617
  for m in out:
618
618
  yield m
619
619
 
620
+ # TODO this is not used until get_dev_identifier is refactored to support type vs generic_type
621
+ # TODO Also args is not being sent by typer, would need to use ctx.params.values()
622
+ # def dev_cx_completion(
623
+ # self,
624
+ # incomplete: str,
625
+ # args: List[str] = [],
626
+ # ) -> Generator[Tuple[str, str], None, None] | None:
627
+ # """Device completion for returning matches that are CX switches
628
+
629
+ # Args:
630
+ # incomplete (str): The last partial or full command before completion invoked.
631
+ # args (List[str], optional): The previous arguments/commands on CLI. Defaults to [].
632
+
633
+ # Yields:
634
+ # Generator[Tuple[str, str], None, None]: Name and help_text for the device, or
635
+ # Returns None if config is invalid
636
+ # """
637
+ # # Prevents exception during completion when config missing or invalid
638
+ # if not config.valid:
639
+ # err_console.print(":warning: Invalid config")
640
+ # return
641
+
642
+ # match = self.get_dev_identifier(incomplete, dev_type=["cx"], completion=True)
643
+
644
+ # out = []
645
+ # if match:
646
+ # out = [
647
+ # tuple([m.name, m.help_text]) for m in sorted(match, key=lambda i: i.name)
648
+ # if m.name not in args
649
+ # ]
650
+
651
+ # for m in out:
652
+ # yield m
653
+
620
654
  def dev_kwarg_completion(
621
655
  self,
622
656
  ctx: typer.Context,
@@ -810,6 +844,7 @@ class Cache:
810
844
 
811
845
  def dev_ap_gw_completion(
812
846
  self,
847
+ ctx: typer.Context,
813
848
  incomplete: str,
814
849
  args: List[str] = None,
815
850
  ) -> Generator[Tuple[str, str], None, None] | None:
@@ -828,6 +863,10 @@ class Cache:
828
863
  err_console.print(":warning: Invalid config")
829
864
  return
830
865
 
866
+ # Prevents device completion for cencli show config cencli
867
+ if ctx.command_path == "cencli show config" and ctx.params.get("group_dev", "") == "cencli":
868
+ return
869
+
831
870
  dev_types = ["ap", "gw"]
832
871
  _match = self.get_dev_identifier(incomplete, dev_type=dev_types, completion=True)
833
872
 
@@ -912,6 +951,7 @@ class Cache:
912
951
  # works in BASH and powershell
913
952
  def group_dev_ap_gw_completion(
914
953
  self,
954
+ ctx: typer.Context,
915
955
  incomplete: str,
916
956
  args: List[str] = None,
917
957
  ) -> Generator[Tuple[str, str], None, None] | None:
@@ -945,12 +985,16 @@ class Cache:
945
985
  out += [tuple([m.name, m.help_text])]
946
986
  # out += [tuple([m.name if " " not in m.name else f"'{m.name}'", m.help_text])] # FIXME completion for names with spaces is now broken, used to work. Change in completion behavior
947
987
 
948
- # FIXME args is now always [] believe this came with click 8
949
- args = [] if args is None else args
950
- if " ".join(args).lower() == "show config" and "cencli".lower().startswith(incomplete):
951
- out += [("cencli", "show cencli configuration")]
952
- if " ".join(args).lower() == "update config" and "cencli".lower().startswith(incomplete):
953
- out += [("cencli", "update cencli configuration")]
988
+ if args:
989
+ if " ".join(args).lower() == "show config" and "cencli".lower().startswith(incomplete):
990
+ out += [("cencli", "show cencli configuration")]
991
+ if " ".join(args).lower() == "update config" and "cencli".lower().startswith(incomplete):
992
+ out += [("cencli", "update cencli configuration")]
993
+ elif ctx.command_path == "cencli show config" and ctx.params.get("group_dev") is None: # typer not sending args fix
994
+ if "cencli".lower().startswith(incomplete):
995
+ out += [("cencli", "update cencli configuration")]
996
+
997
+
954
998
 
955
999
  # partial completion by serial: out appears to have list with expected tuple but does
956
1000
  # not appear in zsh
@@ -1160,8 +1204,8 @@ class Cache:
1160
1204
  out += [tuple([c.name, f'{c.ip}|{c.mac} type: {c.type} connected to: {c.connected_name} ~ {c.connected_port} (fail-safe match)'])]
1161
1205
 
1162
1206
 
1163
- for c in out:
1164
- yield c
1207
+ for c in out: # TODO completion behavior has changed. This works-around issue bash doesn't complete past 00: and zsh treats each octet as a dev name when : is used.
1208
+ yield c[0].replace(":", "-"), c[1]
1165
1209
 
1166
1210
  def event_completion(
1167
1211
  self,
@@ -1183,9 +1227,17 @@ class Cache:
1183
1227
  err_console.print(":warning: Invalid config")
1184
1228
  return
1185
1229
 
1186
- for event in self.events:
1187
- if event["id"].startswith(incomplete):
1188
- yield event["id"], f"{event['id']}|{event['device'].split('Group:')[0].rstrip()}"
1230
+ if incomplete == "":
1231
+ out = [("cencli", "Show cencli logs"), *[(x['id'], f"{x['id']}|{x['device'].split('Group:')[0].rstrip()}") for x in self.events]]
1232
+ for m in out:
1233
+ yield m[0], m[1]
1234
+
1235
+ elif "cencli".startswith(incomplete.lower()):
1236
+ yield "cencli", "Show cencli logs"
1237
+ else:
1238
+ for event in self.events:
1239
+ if event["id"].startswith(incomplete):
1240
+ yield event["id"], f"{event['id']}|{event['device'].split('Group:')[0].rstrip()}"
1189
1241
 
1190
1242
  # TODO add support for zip code city state etc.
1191
1243
  def site_completion(
@@ -1356,6 +1408,7 @@ class Cache:
1356
1408
  for m in out:
1357
1409
  yield m
1358
1410
 
1411
+ # TODO double check not used and remove
1359
1412
  def completion(
1360
1413
  self,
1361
1414
  incomplete: str,
@@ -1428,6 +1481,21 @@ class Cache:
1428
1481
 
1429
1482
  return len(ret) == len(data)
1430
1483
 
1484
+ async def get_swack_ids(self, resp: Response, update_data: List[dict]) -> List[dict]:
1485
+ # TODO these likely cause more delay on large accounts as it's pulling from raw, make more efficient or loop for both aps/switches in the same loop
1486
+ # add swarm_ids for APs to cache (AOS 8 IAP and AP individual Upgrade)
1487
+ if "aps" in resp.raw.get("aps", [{}])[0]:
1488
+ _swarm_ids = {d["serial"]: d["swarm_id"] or d["serial"] for d in resp.raw["aps"][0]["aps"]}
1489
+ update_data = [d if not d["type"] == "ap" or d["serial"] not in _swarm_ids else {**d, **{"swack_id": _swarm_ids[d["serial"]]}} for d in update_data]
1490
+
1491
+ # add stack_ids for switches to cache
1492
+ if "switches" in resp.raw.get("switches", [{}])[0]:
1493
+ _stack_ids = {d["serial"]: d["stack_id"] for d in resp.raw["switches"][0]["switches"]}
1494
+ update_data = [d if d["type"] not in ["cx", "sw"] or d["serial"] not in _stack_ids else {**d, **{"swack_id": _stack_ids[d["serial"]]}} for d in update_data]
1495
+ # FIXME need to update everything that uses AP swarm_id to use swack_id (swarm/stack id)
1496
+
1497
+ return update_data
1498
+
1431
1499
  # FIXME handle no devices in Central yet exception 837 --> cleaner.py 498
1432
1500
  # TODO if we are updating inventory we only need to get those devices types
1433
1501
  async def update_dev_db(self, data: Union[str, List[str], List[dict]] = None, remove: bool = False) -> Union[List[int], Response]:
@@ -1487,20 +1555,8 @@ class Cache:
1487
1555
  if resp.ok:
1488
1556
  if resp.output:
1489
1557
  _update_data = utils.listify(resp.output)
1490
- _update_data = cleaner.get_devices(_update_data)
1491
-
1492
- # TODO these likely cause more delay on large accounts as it's pulling from raw, make more efficient or loop for both aps/switches in the same loop
1493
- # add swarm_ids for APs to cache (AOS 8 IAP and AP individual Upgrade)
1494
- if "aps" in resp.raw.get("aps", [{}])[0]:
1495
- _swarm_ids = {d["serial"]: d["swarm_id"] or d["serial"] for d in resp.raw["aps"][0]["aps"]}
1496
- _update_data = [d if not d["type"] == "ap" or d["serial"] not in _swarm_ids else {**d, **{"swack_id": _swarm_ids[d["serial"]]}} for d in _update_data]
1497
-
1498
- # add stack_ids for switches to cache
1499
- if "switches" in resp.raw.get("switches", [{}])[0]:
1500
- _stack_ids = {d["serial"]: d["stack_id"] for d in resp.raw["switches"][0]["switches"]}
1501
- _update_data = [d if d["type"] not in ["cx", "sw"] or d["serial"] not in _stack_ids else {**d, **{"swack_id": _stack_ids[d["serial"]]}} for d in _update_data]
1502
- # FIXME need to update everything that uses AP swarm_id to use swack_id (swarm/stack id)
1503
-
1558
+ _update_data = cleaner.get_devices(_update_data, cache=True)
1559
+ _update_data = await self.get_swack_ids(resp, _update_data)
1504
1560
 
1505
1561
  self.DevDB.truncate()
1506
1562
  update_res = self.DevDB.insert_multiple(_update_data)
@@ -2580,6 +2636,7 @@ class Cache:
2580
2636
  else:
2581
2637
  return None
2582
2638
 
2639
+ # cencli completion can be removed.. Moved to get_event_identifier
2583
2640
  def get_log_identifier(self, query: str) -> str:
2584
2641
  if "audit_trail" in query:
2585
2642
  return query
@@ -2587,7 +2644,6 @@ class Cache:
2587
2644
  return ["cencli", *[x["id"] for x in self.logs]]
2588
2645
 
2589
2646
  try:
2590
-
2591
2647
  if "cencli".startswith(query.lower()):
2592
2648
  return ["cencli"]
2593
2649
 
@@ -2622,7 +2678,7 @@ class Cache:
2622
2678
  return match[-1]["details"]
2623
2679
 
2624
2680
  except ValueError as e:
2625
- log.error(f"Exception in get_event_identifier {e.__class__.__name__}")
2681
+ log.error(f"Exception in get_event_identifier {e.__class__.__name__}", show=True)
2626
2682
  log.exception(e)
2627
- print(f"[bright_red]Exception[/] in get_event_identifier [dark_orange4]{e.__class__.__name__}[/]")
2683
+ # print(f"[bright_red]Exception[/] in get_event_identifier [dark_orange4]{e.__class__.__name__}[/]")
2628
2684
  raise typer.Exit(1)
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import base64
6
6
  import json
7
7
  import time
8
+ import tablib
9
+ import yaml
8
10
  from asyncio.proactor_events import _ProactorBasePipeTransport
9
11
  from datetime import datetime, timedelta
10
12
  from enum import Enum
@@ -1042,7 +1044,7 @@ class CentralApi(Session):
1042
1044
 
1043
1045
  # TODO cleanup the way raw is combined, see show wids all.
1044
1046
  # TODO add full kwargs and type-hints
1045
- async def get_all_devicesv2(self, **kwargs) -> Response:
1047
+ async def get_all_devicesv2(self, calculate_client_count: bool = True, **kwargs) -> Response:
1046
1048
  """Get all devices from Aruba Central
1047
1049
 
1048
1050
  Returns:
@@ -1058,7 +1060,7 @@ class CentralApi(Session):
1058
1060
  }
1059
1061
  _output = {}
1060
1062
 
1061
- reqs = [self.BatchRequest(self.get_devices, dev_type, **kwargs) for dev_type in dev_types]
1063
+ reqs = [self.BatchRequest(self.get_devices, dev_type, calculate_client_count=calculate_client_count, **kwargs) for dev_type in dev_types]
1062
1064
  res = await self._batch_request(reqs)
1063
1065
  _failure_idxs = [idx for idx, r in enumerate(res) if not r]
1064
1066
  if _failure_idxs:
@@ -1075,7 +1077,7 @@ class CentralApi(Session):
1075
1077
 
1076
1078
  resp = res[-1]
1077
1079
  # TODO pass raw JSON use pydantic models in cleaner for non-verbose, verbose, --clients --stats outputs
1078
- if kwargs.get("calculate_client_count"):
1080
+ if calculate_client_count:
1079
1081
  _output = {k: [{"client_count": inner.get("client_count", "-"), **inner} for inner in utils.listify(v)] for k, v in zip(dev_types, [r.output for r in res]) if v}
1080
1082
  else:
1081
1083
  _output = {k: utils.listify(v) for k, v in zip(dev_types, [r.output for r in res]) if v}
@@ -1105,7 +1107,9 @@ class CentralApi(Session):
1105
1107
 
1106
1108
  return resp
1107
1109
 
1108
- # API-FLAW aos-sw always shows VLAN as 1 (allowed_vlans represents the PVID for access)
1110
+ # API-FLAW aos-sw always shows VLAN as 1 (allowed_vlans represents the PVID for an access port, include all VLANs on a trunk port, no indication of native)
1111
+ # API-FLAW aos-sw always shows mode as access, cx does as well, but has vlan_mode which is accurate
1112
+ # API-FLAW neither show interface name/description
1109
1113
  async def get_switch_ports(self, serial: str, slot: str = None, aos_sw: bool = False) -> Response:
1110
1114
  """Switch Ports Details.
1111
1115
 
@@ -1113,7 +1117,7 @@ class CentralApi(Session):
1113
1117
  serial (str): Serial number of switch to be queried
1114
1118
  slot (str, optional): Slot name of the ports to be queried {For chassis type switches
1115
1119
  only}.
1116
- aos_sw (bool, optional): Device is ArubaOS-Switch. Defaults to False
1120
+ aos_sw (bool, optional): Device is ArubaOS-Switch. Defaults to False (indicating CX switch)
1117
1121
 
1118
1122
  Returns:
1119
1123
  Response: CentralAPI Response object
@@ -1322,7 +1326,7 @@ class CentralApi(Session):
1322
1326
  show_resource_details: bool = False,
1323
1327
  cluster_id: str = None,
1324
1328
  model: str = None,
1325
- calculate_client_count: bool = False,
1329
+ calculate_client_count: bool = True,
1326
1330
  calculate_ssid_count: bool = False,
1327
1331
  macaddr: str = None,
1328
1332
  public_ip_address: str = None,
@@ -1558,20 +1562,9 @@ class CentralApi(Session):
1558
1562
 
1559
1563
  return await self.get(url)
1560
1564
 
1561
- # DELME not used should be safe to remove after verification
1562
- async def get_gateways_by_group(self, group):
1563
- url = "/monitoring/v1/mobility_controllers" if config.is_cop else "/monitoring/v1/gateways"
1564
- params = {"group": group}
1565
- return await self.get(url, params=params)
1566
-
1567
1565
  async def get_group_for_dev_by_serial(self, serial_num):
1568
1566
  return await self.get(f"/configuration/v1/devices/{serial_num}/group")
1569
1567
 
1570
- # async def get_dhcp_client_info_by_gw(self, serial_num):
1571
- # url = f"/monitoring/v1/mobility_controllers/{serial_num}/dhcp_clients"
1572
- # params = {"reservation": False}
1573
- # return await self.get(url, params=params)
1574
-
1575
1568
  async def get_vlan_info_by_gw(self, serial_num):
1576
1569
  return await self.get(f"/monitoring/v1/mobility_controllers/{serial_num}/vlan")
1577
1570
 
@@ -1584,11 +1577,6 @@ class CentralApi(Session):
1584
1577
  url = f"/monitoring/v1/mobility_controllers/{serial_num}/uplinks/tunnel_stats"
1585
1578
  return await self.get(url)
1586
1579
 
1587
- async def get_uplink_state_by_group(self, group: str) -> Response:
1588
- url = "/monitoring/v1/mobility_controllers/uplinks/distribution"
1589
- params = {"group": group}
1590
- return await self.get(url, params)
1591
-
1592
1580
  # TODO move cleaner
1593
1581
  async def get_all_sites(
1594
1582
  self,
@@ -1626,12 +1614,15 @@ class CentralApi(Session):
1626
1614
  event_description: str = None,
1627
1615
  event_type: str = None,
1628
1616
  fields: str = None,
1629
- calculate_total: bool = None,
1617
+ calculate_total: bool = True,
1630
1618
  offset: int = 0,
1631
1619
  limit: int = 1000,
1632
1620
  ) -> Response:
1633
1621
  """List Events. v2
1634
1622
 
1623
+ Endpoint allows a max of 10000 records to be retrieved. The sum of offset + limit can not
1624
+ exceed 10,000
1625
+
1635
1626
  Args:
1636
1627
  group (str, optional): Filter by group name
1637
1628
  swarm_id (str, optional): Filter by Swarm ID
@@ -1655,7 +1646,7 @@ class CentralApi(Session):
1655
1646
  event_type (str, optional): Filter by event type
1656
1647
  fields (str, optional): Comma separated list of fields to be returned. Valid fields are
1657
1648
  number, level
1658
- calculate_total (bool, optional): Whether to calculate total events
1649
+ calculate_total (bool, optional): Whether to calculate total events. Defaults to True.
1659
1650
  offset (int, optional): Pagination offset Defaults to 0.
1660
1651
  limit (int, optional): Pagination limit. Default is 100 and max is 1000 Defaults to 1000.
1661
1652
 
@@ -1668,8 +1659,8 @@ class CentralApi(Session):
1668
1659
  "group": group,
1669
1660
  "swarm_id": swarm_id,
1670
1661
  "label": label,
1671
- "from_tinmestamp": from_ts,
1672
- "to_tinmestamp": to_ts,
1662
+ "from_timestamp": from_ts,
1663
+ "to_timestamp": to_ts,
1673
1664
  'macaddr': macaddr,
1674
1665
  'bssid': bssid,
1675
1666
  'device_mac': device_mac,
@@ -1682,7 +1673,7 @@ class CentralApi(Session):
1682
1673
  'event_description': event_description,
1683
1674
  'event_type': event_type,
1684
1675
  'fields': fields,
1685
- 'calculate_total': calculate_total,
1676
+ 'calculate_total': str(calculate_total),
1686
1677
  "offset": offset,
1687
1678
  "limit": limit,
1688
1679
  }
@@ -1902,29 +1893,6 @@ class CentralApi(Session):
1902
1893
  else:
1903
1894
  return Response(error="Missing Required Parameters")
1904
1895
 
1905
- async def update_ssh_creds(self, device_serial: str, username: str, password: str) -> Response:
1906
- """Set Username, password required for establishing SSH connection to switch.
1907
-
1908
- This method only applies to switches
1909
-
1910
- Args:
1911
- device_serial (str): Serial number of the switch.
1912
- username (str): SSH username
1913
- password (str): SSH password
1914
-
1915
- Returns:
1916
- Response: CentralAPI Response object
1917
- Successful Response body (Response.output): "Success"
1918
- """
1919
- url = f"/configuration/v1/devices/{device_serial}/ssh_connection"
1920
-
1921
- json_data = {
1922
- 'username': username,
1923
- 'password': password
1924
- }
1925
-
1926
- return await self.post(url, json_data=json_data)
1927
-
1928
1896
  async def get_task_status(
1929
1897
  self,
1930
1898
  task_id: str,
@@ -2021,24 +1989,6 @@ class CentralApi(Session):
2021
1989
 
2022
1990
  return await self.get(url)
2023
1991
 
2024
- async def get_controller_vlans(self, serial: str) -> Response:
2025
- """Get Mobility Controllers VLAN details.
2026
-
2027
- Args:
2028
- serial (str): Serial number of mobility controller to be queried
2029
-
2030
- Returns:
2031
- Response: CentralAPI Response object
2032
- """
2033
- url = f"/monitoring/v1/mobility_controllers/{serial}/vlan"
2034
-
2035
- return await self.get(url)
2036
-
2037
- # async def get_ts_commands(self, dev_type: Literal['iap', 'mas', 'switch', 'controller']) -> Response:
2038
- # url = "/troubleshooting/v1/commands"
2039
- # params = {"device_type": dev_type}
2040
- # return await self.get(url, params=params)
2041
-
2042
1992
  async def get_ts_commands(
2043
1993
  self,
2044
1994
  device_type: Literal['iap', 'mas', 'switch', 'controller', 'cx'],
@@ -2196,6 +2146,56 @@ class CentralApi(Session):
2196
2146
 
2197
2147
  return await self.get(url)
2198
2148
 
2149
+ async def get_switch_neighbors_v1(
2150
+ self,
2151
+ serial: str,
2152
+ ) -> Response:
2153
+ """Get lldp device neighbor info for CX switch.
2154
+
2155
+ If used on AOS-SW will only return neighbors that are CX switches
2156
+
2157
+ Args:
2158
+ serial (str): id of the switch
2159
+
2160
+ Returns:
2161
+ Response: CentralAPI Response object
2162
+ """
2163
+ url = f"/monitoring/v1/cx_switches/{serial}/neighbors"
2164
+
2165
+ return await self.get(url)
2166
+
2167
+ async def get_cx_switch_stack_neighbors_v1(
2168
+ self,
2169
+ stack_id: str,
2170
+ ) -> Response:
2171
+ """Get lldp device neighbor info for CX switch stack.
2172
+
2173
+ Args:
2174
+ stack_id (str): Filter by stack_id
2175
+
2176
+ Returns:
2177
+ Response: CentralAPI Response object
2178
+ """
2179
+ url = f"/monitoring/v1/cx_switch_stacks/{stack_id}/neighbors"
2180
+
2181
+ return await self.get(url)
2182
+
2183
+ async def get_switch_vsx_detail(
2184
+ self,
2185
+ serial: str,
2186
+ ) -> Response:
2187
+ """Get switch vsx info for CX switch.
2188
+
2189
+ Args:
2190
+ serial (str): Filter by switch serial
2191
+
2192
+ Returns:
2193
+ Response: CentralAPI Response object
2194
+ """
2195
+ url = f"/monitoring/v1/cx_switches/{serial}/vsx"
2196
+
2197
+ return await self.get(url)
2198
+
2199
2199
  async def do_multi_group_snapshot(
2200
2200
  self,
2201
2201
  backup_name: str,
@@ -3159,7 +3159,7 @@ class CentralApi(Session):
3159
3159
  self,
3160
3160
  swarm_id: str,
3161
3161
  command: Literal[
3162
- "reboot_swarm",
3162
+ "reboot",
3163
3163
  "erase_configuration",
3164
3164
  ]
3165
3165
  ) -> Response:
@@ -3168,11 +3168,14 @@ class CentralApi(Session):
3168
3168
  Args:
3169
3169
  swarm_id (str): Swarm ID of device
3170
3170
  command (str): Command mentioned in the description that is to be executed
3171
- valid: 'reboot_swarm', 'erase_configuration'
3171
+ valid: 'reboot', 'erase_configuration'
3172
3172
 
3173
3173
  Returns:
3174
3174
  Response: CentralAPI Response object
3175
3175
  """
3176
+ if command == "reboot":
3177
+ command = "reboot_swarm"
3178
+
3176
3179
  url = f"/device_management/v1/swarm/{swarm_id}/action/{command}"
3177
3180
 
3178
3181
  return await self.post(url)
@@ -4146,29 +4149,6 @@ class CentralApi(Session):
4146
4149
 
4147
4150
  return await self.delete(url)
4148
4151
 
4149
- # TODO may remove this would show the device in the group, but didn't behave as expected (device was not in device list)
4150
- async def assign_devices_to_group(self, group: str, serial_nums: Union[List[str], str]) -> Response:
4151
- """Assign devices to pre-provisioned group.
4152
-
4153
- // Used indirectly by add device (when group option provided) //
4154
-
4155
- Args:
4156
- group (str): Group name
4157
- serials (List[str]|str): Device serial number or list of device serial numbers.
4158
-
4159
- Returns:
4160
- Response: CentralAPI Response object
4161
- """
4162
- url = "/device_management/v1/group/assign"
4163
- serial_nums = utils.listify(serial_nums)
4164
-
4165
- json_data = {
4166
- 'serials': serial_nums,
4167
- 'group': group
4168
- }
4169
-
4170
- return await self.post(url, json_data=json_data)
4171
-
4172
4152
  async def preprovision_device_to_group(
4173
4153
  self,
4174
4154
  group_name: str,
@@ -5634,6 +5614,44 @@ class CentralApi(Session):
5634
5614
  url = "/keymgmt/v1/health"
5635
5615
  return await self.get(url)
5636
5616
 
5617
+ async def cloudauth_get_registered_macs(
5618
+ self,
5619
+ search: str = None,
5620
+ sort: str = None,
5621
+ filename: str = None,
5622
+ ) -> Response:
5623
+ """Fetch all Mac Registrations as a CSV file.
5624
+
5625
+ Args:
5626
+ search (str, optional): Filter the Mac Registrations by Mac Address and Client Name.
5627
+ Does a 'contains' match.
5628
+ sort (str, optional): Sort order Valid Values: +name, -name, +display_name,
5629
+ -display_name
5630
+ filename (str, optional): Suggest a file name for the downloading file via content
5631
+ disposition header.
5632
+
5633
+ Returns:
5634
+ Response: CentralAPI Response object
5635
+ """
5636
+ url = "/cloudauth/api/v3/bulk/mac"
5637
+
5638
+ params = {
5639
+ 'search': search,
5640
+ 'sort': sort,
5641
+ 'filename': filename
5642
+ }
5643
+
5644
+ resp = await self.get(url, params=params)
5645
+
5646
+ if resp:
5647
+ try:
5648
+ ds = tablib.Dataset().load(resp.output)
5649
+ resp.output = yaml.load(ds.json, Loader=yaml.SafeLoader)
5650
+ except Exception as e:
5651
+ log.error(f"cloudauth_get_registered_macs caught {e.__class__.__name__} trying to convert csv return from API to dict.", caption=True)
5652
+
5653
+ return resp
5654
+
5637
5655
  async def get_user_accounts(
5638
5656
  self,
5639
5657
  app_name: str = None,
@@ -5672,7 +5690,10 @@ class CentralApi(Session):
5672
5690
  'limit': limit
5673
5691
  }
5674
5692
 
5675
- return await self.get(url, params=params)
5693
+ # TODO this needs a fair amount of massaging to turn into a command, it's nested dicts
5694
+ # example response in private vscode dir.
5695
+ resp = await self.get(url, params=params)
5696
+ return resp
5676
5697
 
5677
5698
  if __name__ == "__main__":
5678
5699
  pass