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.
- {centralcli-5.2.2 → centralcli-5.3.0}/PKG-INFO +1 -1
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cache.py +1 -1
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/central.py +3 -3
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cleaner.py +15 -18
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clibatch.py +14 -3
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clicommon.py +4 -2
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishow.py +31 -27
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowaudit.py +5 -4
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowospf.py +24 -37
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowoverlay.py +12 -13
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliupgrade.py +5 -3
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/constants.py +1 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/render.py +2 -2
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/response.py +36 -18
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/strings.py +0 -15
- {centralcli-5.2.2 → centralcli-5.3.0}/pyproject.toml +1 -1
- {centralcli-5.2.2 → centralcli-5.3.0}/LICENSE +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/README.md +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/__init__.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/boilerplate/README.md +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/boilerplate/allcalls.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/caas.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cli.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliadd.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliassign.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clicaas.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliclone.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clidel.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clidelfirmware.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliexport.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clikick.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clioptions.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clirefresh.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clirename.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliset.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clisetfirmware.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowbranch.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowcloudauth.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowfirmware.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowmpsk.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowtshoot.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clishowwids.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clitest.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/clitshoot.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliunassign.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/cliupdate.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/config.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/exceptions.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/logger.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/models.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/objects.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/setup.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/static/favicon.ico +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/utils.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/vscodeargs.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/wh2snow.py +0 -0
- {centralcli-5.2.2 → centralcli-5.3.0}/centralcli/wh_proxy.py +0 -0
- {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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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':
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
"""
|
|
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"
|
|
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=
|
|
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
|
-
|
|
994
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1928
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
120
|
-
log_id = cli.cache.get_log_identifier(
|
|
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'
|
|
71
|
-
f'
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
cli.display_results(\
|
|
74
|
+
cli.display_results(
|
|
78
75
|
resp,
|
|
79
76
|
tablefmt=tablefmt,
|
|
80
|
-
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=
|
|
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
|
-
|
|
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'
|
|
134
|
-
f'
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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'
|
|
196
|
-
f'
|
|
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=
|
|
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=
|
|
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'{
|
|
258
|
-
f'
|
|
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=
|
|
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=
|
|
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.
|
|
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
|
|
174
|
+
"""Show overlay connection (OTO/ORO) details (Valid on SD-Branch GWs/ VPNCs Only)
|
|
177
175
|
|
|
178
|
-
|
|
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=
|
|
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 = {
|
|
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
|
-
|
|
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,
|
|
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",),
|
|
@@ -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
|
|
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
|
-
|
|
164
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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"?
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
if
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|