centralcli 8.2.2__tar.gz → 8.2.4__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-8.2.2 → centralcli-8.2.4}/PKG-INFO +1 -1
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cache.py +54 -8
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cleaner.py +0 -7
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clibatch.py +7 -3
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicommon.py +97 -1
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishow.py +13 -6
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/models.py +2 -2
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/render.py +58 -26
- {centralcli-8.2.2 → centralcli-8.2.4}/pyproject.toml +1 -1
- centralcli-8.2.2/centralcli/cnxcli/__init__.py +0 -25
- centralcli-8.2.2/centralcli/cnxcli/api/__init__.py +0 -12
- centralcli-8.2.2/centralcli/cnxcli/api/cnxapi.py +0 -488
- centralcli-8.2.2/centralcli/cnxcli/cli.py +0 -71
- centralcli-8.2.2/centralcli/cnxcli/client.py +0 -706
- centralcli-8.2.2/centralcli/cnxcli/clioptions.py +0 -66
- centralcli-8.2.2/centralcli/cnxcli/clitree/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/clitree/assign/__init__.py +0 -27
- centralcli-8.2.2/centralcli/cnxcli/clitree/assign/assign.py +0 -74
- centralcli-8.2.2/centralcli/cnxcli/clitree/common.py +0 -96
- centralcli-8.2.2/centralcli/cnxcli/clitree/show/__init__.py +0 -28
- centralcli-8.2.2/centralcli/cnxcli/clitree/show/auth.py +0 -48
- centralcli-8.2.2/centralcli/cnxcli/clitree/show/show.py +0 -181
- centralcli-8.2.2/centralcli/cnxcli/clitree/show/switch.py +0 -64
- centralcli-8.2.2/centralcli/cnxcli/clitree/update/__init__.py +0 -27
- centralcli-8.2.2/centralcli/cnxcli/clitree/update/auth.py +0 -121
- centralcli-8.2.2/centralcli/cnxcli/clitree/update/update.py +0 -343
- centralcli-8.2.2/centralcli/cnxcli/cnx_cleaner.py +0 -85
- centralcli-8.2.2/centralcli/cnxcli/models/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/models/auth_server_global.py +0 -719
- centralcli-8.2.2/centralcli/cnxcli/models/local_management.py +0 -5415
- centralcli-8.2.2/centralcli/cnxcli/models/system_info.py +0 -396
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/base.py +0 -260
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/base_utils.py +0 -188
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/audit_logs.py +0 -182
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/base.py +0 -689
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/base_utils.py +0 -250
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/configuration.py +0 -1389
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/constants.py +0 -42
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/device_inventory.py +0 -252
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/firmware_management.py +0 -277
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/licensing.py +0 -488
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/monitoring.py +0 -313
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/msp.py +0 -891
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/rapids.py +0 -513
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/refresh_api_token.py +0 -67
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/topology.py +0 -145
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/url_utils.py +0 -244
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/user_management.py +0 -427
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/visualrf.py +0 -351
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/workflows/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/classic/workflows/workflows_utils.py +0 -154
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/configuration/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/__init__.py +0 -6
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/generic_op_error.py +0 -35
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/login_error.py +0 -30
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/parameter_error.py +0 -11
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/pycentral_error.py +0 -31
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/response_error.py +0 -32
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/unsupported_capability_error.py +0 -12
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/exceptions/verification_error.py +0 -31
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/__init__.py +0 -3
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/devices.py +0 -556
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/glp_utils.py +0 -80
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/service_manager.py +0 -44
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/subscription.py +0 -196
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/glp/user_management.py +0 -142
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/monitoring/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/__init__.py +0 -3
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/ntp.py +0 -19
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/profiles.py +0 -234
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/system_info.py +0 -291
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/vlan.py +0 -278
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/profiles/wlan.py +0 -20
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/__init__.py +0 -6
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/device.py +0 -92
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/scope_maps.py +0 -207
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/scope_utils.py +0 -338
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/scopes.py +0 -1053
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/site.py +0 -396
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/scopes/site_collection.py +0 -435
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/assign_vlan_profile_site.py +0 -131
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/assign_wlan_profile_site.py +0 -115
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/account_credentials.json +0 -14
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/cleanup_site_profile_workflow.py +0 -84
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/profile_config.json +0 -65
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/site_profile_workflow.py +0 -103
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-cnx-onboarding/sites_config.json +0 -38
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/account_credentials.json +0 -16
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/glp_onboarding.py +0 -219
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/reset/old_account_credentials.json +0 -16
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/reset/region_name_mapping.json +0 -14
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/atm-glp-onboarding/workflow_vars.json +0 -33
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/account_credentials.json +0 -14
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/cleanup_exception.py +0 -42
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/exception_test.py +0 -63
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/exception_test/profile_config.json +0 -65
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp/test_glp.py +0 -354
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp/test_glp_vars.json +0 -115
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp-access-token-example.py +0 -19
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/glp-client-creds-example.py +0 -20
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/central_token.json +0 -7
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/profile_config.json +0 -21
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/system_info_profile.py +0 -83
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/profiles/system_info_profile_cleanup.py +0 -83
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scope-management-example.py +0 -171
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scope-management-vars.json +0 -24
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scopes/__init__.py +0 -0
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scopes/test_scopes.py +0 -706
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/tests/scopes/test_scopes_vars.json +0 -185
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/url_utils.py +0 -83
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/utils/__init__.py +0 -1
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/utils/common_utils.py +0 -41
- centralcli-8.2.2/centralcli/cnxcli/pycentralv2/utils/url_utils.py +0 -107
- {centralcli-8.2.2 → centralcli-8.2.4}/LICENSE +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/README.md +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/__init__.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/boilerplate/README.md +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/boilerplate/_cnx_allcalls.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/boilerplate/allcalls.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/caas.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/central.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cli.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliadd.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliassign.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicaas.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicancel.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clicheck.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliclone.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clidel.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clidelfirmware.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliexport.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clikick.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clioptions.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clirefresh.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clirename.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliset.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clisetfirmware.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowaudit.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowbandwidth.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowbranch.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowcloudauth.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowfirmware.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowmpsk.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowospf.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowoverlay.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowtshoot.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clishowwids.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clitest.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/clitshoot.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliunassign.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliupdate.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/cliupgrade.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/config.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/constants.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/exceptions.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/logger.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/objects.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/response.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/setup.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/static/favicon.ico +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/strings.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/typedefs.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/utils.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/vendored/csvlexer/__init__.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/vendored/csvlexer/csv.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/vscodeargs.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/wh2snow.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/wh_proxy.py +0 -0
- {centralcli-8.2.2 → centralcli-8.2.4}/centralcli/wh_proxy_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: centralcli
|
|
3
|
-
Version: 8.2.
|
|
3
|
+
Version: 8.2.4
|
|
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
|
License: MIT
|
|
6
6
|
Keywords: cli,Aruba,Aruba Networks,Aruba Central,HPE,API,RESTFUL,REST
|
|
@@ -2537,14 +2537,16 @@ class Cache:
|
|
|
2537
2537
|
return await self.update_db(DB, data=list(updated_devs_by_serial.values()), truncate=True)
|
|
2538
2538
|
|
|
2539
2539
|
|
|
2540
|
-
async def update_db(self, db: Table, data: List[Dict[str, Any]] | Dict[str, Any] = None, doc_ids: List[int] | int = None, truncate: bool = True) -> bool:
|
|
2540
|
+
async def update_db(self, db: Table, data: List[Dict[str, Any]] | Dict[str, Any] = None, *, doc_ids: List[int] | int = None, dev_types: constants.GenericDeviceTypes | List[constants.GenericDeviceTypes] = None, truncate: bool = True,) -> bool:
|
|
2541
2541
|
"""Update Local Cache DB
|
|
2542
2542
|
|
|
2543
2543
|
Args:
|
|
2544
2544
|
db (Table): TinyDB Table object to be updated.
|
|
2545
2545
|
data (List[Dict[str, Any]] | Dict[str, Any], optional): Data to be added to database. Defaults to None.
|
|
2546
|
-
truncate (bool, optional): Existing DB data will be discarded, and all data in DB will be replaced with provided. Defaults to True.
|
|
2547
2546
|
doc_ids (List[int] | int, optional): doc_ids to be deleted from the DB. Defaults to None.
|
|
2547
|
+
dev_types (Literal["ap", "gw", "cx", "sw", "switch"] | List["ap", "gw" ...], optional): List of dev_types the data represents as current for those types.
|
|
2548
|
+
This will result in any devices of the specified types that do not exist in the provided data being removed from cache. Defaults to None.
|
|
2549
|
+
truncate (bool, optional): Existing DB data will be discarded, and all data in DB will be replaced with provided. Defaults to True.
|
|
2548
2550
|
|
|
2549
2551
|
Returns:
|
|
2550
2552
|
bool: _description_
|
|
@@ -2589,6 +2591,43 @@ class Cache:
|
|
|
2589
2591
|
else:
|
|
2590
2592
|
return await self.update_db(self.DevDB, doc_ids=data)
|
|
2591
2593
|
|
|
2594
|
+
async def prep_filtered_devs_for_cache(self, raw_models: List[models.Device], dev_type: constants.GenericDeviceTypes | List[constants.GenericDeviceTypes] = None, site: str = None, group: str = None) -> List[dict]:
|
|
2595
|
+
new_by_serial = {d.serial: d.model_dump() for d in raw_models}
|
|
2596
|
+
filters = {
|
|
2597
|
+
"dev_type": dev_type,
|
|
2598
|
+
"site": site,
|
|
2599
|
+
"group": group
|
|
2600
|
+
}
|
|
2601
|
+
filter_msg = ", ".join([f"{k}: {v if k != 'dev_type' else utils.unlistify(v)}" for k, v in filters.items() if v])
|
|
2602
|
+
|
|
2603
|
+
if dev_type:
|
|
2604
|
+
switch_types = ["cx", "sw"] if "switch" in dev_type else []
|
|
2605
|
+
cache_type = [*[t for t in dev_type if t != "switch"], *switch_types]
|
|
2606
|
+
else:
|
|
2607
|
+
cache_type = []
|
|
2608
|
+
|
|
2609
|
+
def include_device(dev: dict) -> bool:
|
|
2610
|
+
criteria = []
|
|
2611
|
+
if cache_type:
|
|
2612
|
+
criteria += [dev["type"] in cache_type]
|
|
2613
|
+
if site:
|
|
2614
|
+
criteria += [dev["site"] == site]
|
|
2615
|
+
if group:
|
|
2616
|
+
criteria += [dev["group"] == group]
|
|
2617
|
+
|
|
2618
|
+
if all(criteria):
|
|
2619
|
+
if dev["serial"] not in new_by_serial:
|
|
2620
|
+
return False
|
|
2621
|
+
|
|
2622
|
+
return True
|
|
2623
|
+
|
|
2624
|
+
cache_devices = {cd["serial"]: cd for cd in self.devices if include_device(cd)}
|
|
2625
|
+
|
|
2626
|
+
update_data = {**cache_devices, **new_by_serial}
|
|
2627
|
+
log.info(f"Data prepared for device cache update. Filters: {filter_msg}. Add/update {len(new_by_serial)} devices. Devices in cache: Now: {len(self.devices)}, After Update: {len(update_data)}.")
|
|
2628
|
+
|
|
2629
|
+
return list(update_data.values())
|
|
2630
|
+
|
|
2592
2631
|
async def refresh_dev_db(
|
|
2593
2632
|
self,
|
|
2594
2633
|
dev_type: constants.GenericDeviceTypes | List[constants.GenericDeviceTypes] = None, # TODO make consistent throughout using device_type in many places
|
|
@@ -2657,7 +2696,9 @@ class Cache:
|
|
|
2657
2696
|
offset=offset,
|
|
2658
2697
|
limit=limit,
|
|
2659
2698
|
)
|
|
2660
|
-
if isinstance(resp, CombinedResponse) and resp.ok:
|
|
2699
|
+
if isinstance(resp, CombinedResponse) and resp.ok: # Can be Response | List[Response] if get_all_devices aborted due to failures
|
|
2700
|
+
# Any filters not in list below do not result in a cache update
|
|
2701
|
+
filtered_resonse = True if any([label, serial, mac, model, stack_id, swarm_id, cluster_id, public_ip_address, status]) else False
|
|
2661
2702
|
raw_data = await self.format_raw_devices_for_cache(resp)
|
|
2662
2703
|
with console.status(f"preparing {len(resp)} records for cache update"):
|
|
2663
2704
|
_start_time = time.perf_counter()
|
|
@@ -2665,13 +2706,18 @@ class Cache:
|
|
|
2665
2706
|
raw_models = [*raw_models_by_type.aps, *raw_models_by_type.switches, *raw_models_by_type.gateways]
|
|
2666
2707
|
log.debug(f"prepared {len(resp)} records for dev cache update in {round(time.perf_counter() - _start_time, 2)}")
|
|
2667
2708
|
|
|
2668
|
-
|
|
2709
|
+
if dev_type:
|
|
2710
|
+
update_data = await self.prep_filtered_devs_for_cache(raw_models=raw_models, dev_type=dev_type, site=site, group=group)
|
|
2711
|
+
else:
|
|
2712
|
+
update_data = [dev.model_dump() for dev in raw_models]
|
|
2713
|
+
|
|
2669
2714
|
if resp.all_ok and not filtered_resonse:
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2715
|
+
if not dev_type:
|
|
2716
|
+
self.updated.append(self.central.get_all_devices)
|
|
2717
|
+
self.responses.dev = resp
|
|
2718
|
+
_ = await self.update_db(self.DevDB, data=update_data, truncate=True)
|
|
2673
2719
|
else: # Response is filtered or incomplete due to partial failure merge with existing cache data (update)
|
|
2674
|
-
_ = await self._add_update_devices(
|
|
2720
|
+
_ = await self._add_update_devices(update_data)
|
|
2675
2721
|
|
|
2676
2722
|
return resp
|
|
2677
2723
|
|
|
@@ -2069,14 +2069,7 @@ def get_swarm_firmware_details(data: List[Dict[str, Any]]) -> List[Dict[str, Any
|
|
|
2069
2069
|
return simple_kv_formatter(data, key_order=key_order)
|
|
2070
2070
|
|
|
2071
2071
|
def show_radios(data: List[Dict[str, str | int]]) -> List[Dict[str, str | int]]:
|
|
2072
|
-
def by_name_blocks(name: str, exist_names=[]) -> str:
|
|
2073
|
-
if name not in exist_names:
|
|
2074
|
-
exist_names += [name]
|
|
2075
|
-
return name
|
|
2076
|
-
return ''
|
|
2077
|
-
|
|
2078
2072
|
key_order = ["name", "macaddr", "radio_name", "status", "channel", "radio_type", "spatial_stream", "mode", "tx_power", "utilization",] # "band", "index"]
|
|
2079
|
-
data = [{k: v if k != "name" else by_name_blocks(v) for k, v in inner.items()} for inner in data]
|
|
2080
2073
|
data = simple_kv_formatter(data, key_order=key_order)
|
|
2081
2074
|
|
|
2082
2075
|
return data
|
|
@@ -378,7 +378,7 @@ def batch_add_sites(import_file: Path = None, data: dict = None, yes: bool = Fal
|
|
|
378
378
|
address_fields = {"site_name": "bright_green", "name": "bright_green", "address": "bright_cyan", "city": "turquoise4", "state": "dark_olive_green3", "country": "magenta", "zipcode": "blue", "zip": "blue"}
|
|
379
379
|
confirm_msg = utils.summarize_list(
|
|
380
380
|
[
|
|
381
|
-
"|".join([f'[{address_fields[k]}]{v}[/]' for k, v in site.items() if v and k in address_fields]) for site in verified_sites
|
|
381
|
+
"|".join([f'[{address_fields[k]}]{v}[/]' for k, v in site.model_dump().items() if v and k in address_fields]) for site in list(verified_sites)
|
|
382
382
|
],
|
|
383
383
|
max=7
|
|
384
384
|
)
|
|
@@ -974,8 +974,12 @@ def add(
|
|
|
974
974
|
caption, tablefmt = None, "action"
|
|
975
975
|
if what == "sites":
|
|
976
976
|
resp = batch_add_sites(import_file, yes=yes)
|
|
977
|
+
if resp.ok:
|
|
978
|
+
try:
|
|
979
|
+
resp.output = cleaner.sites(resp.output)
|
|
980
|
+
except Exception as e:
|
|
981
|
+
log.error(f"Error cleaning output of batch site addition {repr(e)}", caption=True, log=True)
|
|
977
982
|
tablefmt = "rich"
|
|
978
|
-
# TODO should re-order columns so output is more consistent with show sites (i.e. site_name is not guaranteed to be first col in output of these calls)
|
|
979
983
|
elif what == "groups":
|
|
980
984
|
resp = batch_add_groups(import_file, yes=yes)
|
|
981
985
|
elif what == "devices":
|
|
@@ -1004,7 +1008,7 @@ def add(
|
|
|
1004
1008
|
"Use [cyan]cencli show cloud-auth upload mpsk[/] to see the status of the import."
|
|
1005
1009
|
)
|
|
1006
1010
|
|
|
1007
|
-
cli.display_results(resp, tablefmt=tablefmt, title=f"Batch Add {what}", caption=caption)
|
|
1011
|
+
cli.display_results(resp, tablefmt=tablefmt, title=f"Batch Add {what.value}", caption=caption)
|
|
1008
1012
|
|
|
1009
1013
|
|
|
1010
1014
|
# TODO archive and unarchive have the same block this is used by batch delete
|
|
@@ -521,6 +521,7 @@ class CLICommon:
|
|
|
521
521
|
reverse: bool = False,
|
|
522
522
|
stash: bool = True,
|
|
523
523
|
output_by_key: str | List[str] = "name",
|
|
524
|
+
group_by: str = None,
|
|
524
525
|
set_width_cols: dict = None,
|
|
525
526
|
full_cols: Union[List[str], str] = [],
|
|
526
527
|
fold_cols: Union[List[str], str] = [],
|
|
@@ -555,6 +556,7 @@ class CLICommon:
|
|
|
555
556
|
"account": None if config.account in ["central_info", "default"] else config.account,
|
|
556
557
|
"config": config,
|
|
557
558
|
"output_by_key": output_by_key,
|
|
559
|
+
"group_by": group_by,
|
|
558
560
|
"set_width_cols": set_width_cols,
|
|
559
561
|
"full_cols": full_cols,
|
|
560
562
|
"fold_cols": fold_cols,
|
|
@@ -592,6 +594,7 @@ class CLICommon:
|
|
|
592
594
|
reverse: bool = False,
|
|
593
595
|
stash: bool = True,
|
|
594
596
|
output_by_key: str | List[str] = "name",
|
|
597
|
+
group_by: str = None,
|
|
595
598
|
exit_on_fail: bool = False, # TODO make default True so failed calls return a failed return code to the shell. Need to validate everywhere it needs to be set to False
|
|
596
599
|
cache_update_pending: bool = False,
|
|
597
600
|
set_width_cols: dict = None,
|
|
@@ -625,6 +628,8 @@ class CLICommon:
|
|
|
625
628
|
show last. Default: True
|
|
626
629
|
output_by_key: For json or yaml output, if any of the provided keys are foound in the List of dicts
|
|
627
630
|
the List will be converted to a Dict[value of provided key, original_inner_dict]. Defaults to name.
|
|
631
|
+
group_by: When provided output will be grouped by this key. For outputs where multiple entries relate to a common device, and multiple devices exist in the output.
|
|
632
|
+
i.e. interfaces for a device when the output contains multiple devices. Results in special formatting. Defaults to None
|
|
628
633
|
exit_on_fail: (bool, optional): If provided resp indicates a failure exit after display. Defaults to False
|
|
629
634
|
cache_update_pending: (bool, optional): If a cache update is to be performed if resp is success.
|
|
630
635
|
Results in a warning before exit if failure. Defaults to False
|
|
@@ -753,6 +758,7 @@ class CLICommon:
|
|
|
753
758
|
reverse=reverse,
|
|
754
759
|
stash=stash,
|
|
755
760
|
output_by_key=output_by_key,
|
|
761
|
+
group_by=group_by,
|
|
756
762
|
set_width_cols=set_width_cols,
|
|
757
763
|
full_cols=full_cols,
|
|
758
764
|
fold_cols=fold_cols,
|
|
@@ -1509,7 +1515,7 @@ class CLICommon:
|
|
|
1509
1515
|
cache_devs += [this_dev]
|
|
1510
1516
|
|
|
1511
1517
|
not_found_devs: List[str] = [s for s, c in zip(serials_in, cache_devs) if c is None]
|
|
1512
|
-
cache_found_devs: List[CacheDevice | CacheInvDevice] = [d for d in cache_devs if d]
|
|
1518
|
+
cache_found_devs: List[CacheDevice | CacheInvDevice] = [d for d in cache_devs if d is not None]
|
|
1513
1519
|
cache_mon_devs: List[CacheDevice] = [d for d in cache_found_devs if d.db.name == "devices"]
|
|
1514
1520
|
cache_inv_devs: List[CacheInvDevice] = [d for d in cache_found_devs if d.db.name == "inventory"]
|
|
1515
1521
|
|
|
@@ -1616,6 +1622,96 @@ class CLICommon:
|
|
|
1616
1622
|
if inv_doc_ids:
|
|
1617
1623
|
self.central.request(self.cache.update_inv_db, inv_doc_ids, remove=True)
|
|
1618
1624
|
|
|
1625
|
+
# Header rows used by CAS
|
|
1626
|
+
#DEVICE NAME,SERIAL,MAC,GROUP,SITE,LABELS,LICENSE,ZONE,SWARM MODE,RF PROFILE,INSTALLATION TYPE,RADIO 0 MODE,RADIO 1 MODE,RADIO 2 MODE,DUAL 5GHZ MODE,SPLIT 5GHZ MODE,FLEX DUAL BAND,ANTENNA WIDTH,ALTITUDE,IP ADDRESS,SUBNET MASK,DEFAULT GATEWAY,DNS SERVER,DOMAIN NAME,TIMEZONE,AP1X USERNAME,AP1X PASSWORD
|
|
1627
|
+
|
|
1628
|
+
# def batch_update_aps(self, data: List[dict]) -> List[Response]:
|
|
1629
|
+
# data: List[CacheDevice] = [self.cache.get_dev_identifier(ap, dev_type="ap") for ap in data]
|
|
1630
|
+
|
|
1631
|
+
# disable_radios = None if not disable_radios else [r.value for r in disable_radios]
|
|
1632
|
+
# enable_radios = None if not enable_radios else [r.value for r in enable_radios]
|
|
1633
|
+
# flex_dual_exclude = None if not flex_dual_exclude else flex_dual_exclude.value
|
|
1634
|
+
# antenna_width = None if not antenna_width else antenna_width.value
|
|
1635
|
+
|
|
1636
|
+
# radio_24_disable = None if not enable_radios or "2.4" not in enable_radios else False
|
|
1637
|
+
# radio_5_disable = None if not enable_radios or "5" not in enable_radios else False
|
|
1638
|
+
# radio_6_disable = None if not enable_radios or "6" not in enable_radios else False
|
|
1639
|
+
# if disable_radios:
|
|
1640
|
+
# for radio, var in zip(["2.4", "5", "6"], [radio_24_disable, radio_5_disable, radio_6_disable]):
|
|
1641
|
+
# if radio in disable_radios and var is not None:
|
|
1642
|
+
# cli.exit(f"Invalid combination you tried to enable and disable the {radio}Ghz radio")
|
|
1643
|
+
# # var = None if radio not in disable_radios else True # doesn't work
|
|
1644
|
+
# radio_24_disable = None if "2.4" not in disable_radios else True
|
|
1645
|
+
# radio_5_disable = None if "5" not in disable_radios else True
|
|
1646
|
+
# radio_6_disable = None if "6" not in disable_radios else True
|
|
1647
|
+
|
|
1648
|
+
|
|
1649
|
+
# kwargs = {
|
|
1650
|
+
# "hostname": hostname,
|
|
1651
|
+
# "ip": ip,
|
|
1652
|
+
# "mask": mask,
|
|
1653
|
+
# "gateway": gateway,
|
|
1654
|
+
# "dns": dns,
|
|
1655
|
+
# "domain": domain,
|
|
1656
|
+
# "radio_24_disable": radio_24_disable,
|
|
1657
|
+
# "radio_5_disable": radio_5_disable,
|
|
1658
|
+
# "radio_6_disable": radio_6_disable,
|
|
1659
|
+
# "uplink_vlan": tagged_uplink_vlan,
|
|
1660
|
+
# "flex_dual_exclude": flex_dual_exclude,
|
|
1661
|
+
# "dynamic_ant_mode": antenna_width,
|
|
1662
|
+
# }
|
|
1663
|
+
# if ip and not all([mask, gateway, dns]):
|
|
1664
|
+
# cli.exit("[cyan]mask[/], [cyan]gateway[/], and [cyan]--dns[/] are required when [cyan]--ip[/] is provided.")
|
|
1665
|
+
# if len(data) > 1 and hostname or ip:
|
|
1666
|
+
# cli.exit("Setting hostname/ip on multiple APs doesn't make sesnse")
|
|
1667
|
+
|
|
1668
|
+
# print(f"[bright_green]Updating[/]: {utils.summarize_list([ap.summary_text for ap in data], color=None, pad=10).lstrip()}")
|
|
1669
|
+
# print("\n[green italic]With the following per-ap-settings[/]:")
|
|
1670
|
+
# _ = [print(f" {k}: {v}") for k, v in kwargs.items() if v is not None]
|
|
1671
|
+
# skip_flex = [ap for ap in data if ap.model not in flex_dual_models]
|
|
1672
|
+
# skip_width = [ap for ap in data if ap.model not in ["679"]]
|
|
1673
|
+
|
|
1674
|
+
# warnings = []
|
|
1675
|
+
# if flex_dual_exclude is not None and skip_flex:
|
|
1676
|
+
# warnings += [f"[yellow]:information:[/] Flexible dual radio [red]will be ignored[/] for {len(skip_flex)} AP, as the setting doesn't apply to those models."]
|
|
1677
|
+
# if antenna_width is not None and skip_width:
|
|
1678
|
+
# warnings += [f"[yellow]:information:[/] Dynamic antenna width [red]will be ignored[/] for {len(skip_width)} AP, as the setting doesn't apply to those models."]
|
|
1679
|
+
# if warnings:
|
|
1680
|
+
# warn_text = '\n'.join(warnings)
|
|
1681
|
+
# print(f"\n{warn_text}")
|
|
1682
|
+
|
|
1683
|
+
# # determine if any effective changes after skips for settings on invalid AP models
|
|
1684
|
+
# changes = 2
|
|
1685
|
+
# if not list(filter(None, list(kwargs.values())[0:-2])):
|
|
1686
|
+
# if not flex_dual_exclude or (flex_dual_exclude and not [ap for ap in data if ap not in skip_flex]):
|
|
1687
|
+
# changes -= 1
|
|
1688
|
+
# if not antenna_width or (antenna_width and not [ap for ap in data if ap not in skip_width]):
|
|
1689
|
+
# changes -= 1
|
|
1690
|
+
# if not changes:
|
|
1691
|
+
# cli.exit("No valid updates provided for the selected AP models... Nothing to do.")
|
|
1692
|
+
|
|
1693
|
+
# self.confirm(yes) # exits here if they abort
|
|
1694
|
+
# batch_resp = cli.central.batch_request(
|
|
1695
|
+
# [
|
|
1696
|
+
# BatchRequest(
|
|
1697
|
+
# cli.central.update_per_ap_settings,
|
|
1698
|
+
# ap.serial,
|
|
1699
|
+
# hostname=hostname,
|
|
1700
|
+
# ip=ip,
|
|
1701
|
+
# mask=mask,
|
|
1702
|
+
# gateway=gateway,
|
|
1703
|
+
# dns=dns,
|
|
1704
|
+
# domain=domain,
|
|
1705
|
+
# radio_24_disable=radio_24_disable,
|
|
1706
|
+
# radio_5_disable=radio_5_disable,
|
|
1707
|
+
# radio_6_disable=radio_6_disable,
|
|
1708
|
+
# uplink_vlan=tagged_uplink_vlan,
|
|
1709
|
+
# flex_dual_exclude=None if ap.model not in flex_dual_models else flex_dual_exclude,
|
|
1710
|
+
# dynamic_ant_mode=None if ap.model != "679" else antenna_width,
|
|
1711
|
+
# ) for ap in data
|
|
1712
|
+
# ]
|
|
1713
|
+
# )
|
|
1714
|
+
|
|
1619
1715
|
def help_default(self, default_txt: str) -> str:
|
|
1620
1716
|
"""Helper function that returns properly escaped default text, including rich color markup, for use in CLI help.
|
|
1621
1717
|
|
|
@@ -962,7 +962,11 @@ def parse_interface_responses(dev_type: GenericDeviceTypes, responses: List[Resp
|
|
|
962
962
|
_passed = responses if not _failed else [r for r in responses if r.ok]
|
|
963
963
|
|
|
964
964
|
if _failed:
|
|
965
|
-
|
|
965
|
+
try:
|
|
966
|
+
log.warning(f"Incomplete output!! {len(_failed)} calls failed. Devices: {utils.color([r.url.path.split('/')[-2:][0] for r in _failed])}. [cyan]cencli show logs --cencli[/] for details.", caption=True)
|
|
967
|
+
except Exception:
|
|
968
|
+
log.warning("Incomplete output, failures occured, see log")
|
|
969
|
+
|
|
966
970
|
|
|
967
971
|
# output = [i for r in _passed for i in utils.listify(r.output)]
|
|
968
972
|
output = [r.output for r in _passed]
|
|
@@ -1132,10 +1136,10 @@ def interfaces(
|
|
|
1132
1136
|
|
|
1133
1137
|
caption = []
|
|
1134
1138
|
if dev_type == "switch":
|
|
1135
|
-
if "sw" in [d.type for d in devs]:
|
|
1139
|
+
if "sw" in [d.type for d in devs] and resp.ok:
|
|
1136
1140
|
dev_type = dev_type if len(batch_resp) > 1 else "sw" # So single device cleaner gets specific dev_type
|
|
1137
1141
|
caption = [render.rich_capture(":information: Native VLAN for trunk ports not shown for aos-sw as not provided by the API", emoji=True)]
|
|
1138
|
-
if "cx" in [d.type for d in devs]:
|
|
1142
|
+
if "cx" in [d.type for d in devs] and resp.ok:
|
|
1139
1143
|
caption = [render.rich_capture(":information: L3 interfaces for CX switches will show as Access/VLAN 1 as the L3 details are not provided by the API", emoji=True)]
|
|
1140
1144
|
|
|
1141
1145
|
if resp:
|
|
@@ -1161,6 +1165,7 @@ def interfaces(
|
|
|
1161
1165
|
sort_by=sort_by,
|
|
1162
1166
|
reverse=reverse,
|
|
1163
1167
|
output_by_key=None,
|
|
1168
|
+
group_by=None if len(batch_resp) <= 1 else "device",
|
|
1164
1169
|
cleaner=cleaner.show_interfaces if len(batch_resp) == 1 else None, # Multi device listing is ran through cleaner already
|
|
1165
1170
|
verbosity=verbose,
|
|
1166
1171
|
dev_type=dev_type,
|
|
@@ -3201,22 +3206,24 @@ def radios(
|
|
|
3201
3206
|
resp = sorted(passed, key=lambda ap: ap.rl)[0]
|
|
3202
3207
|
resp.output = [{"name": ap["name"], **rdict} for ap in combined for rdict in ap["radios"]]
|
|
3203
3208
|
if failed:
|
|
3204
|
-
cli.display_results(failed)
|
|
3209
|
+
cli.display_results(failed, tablefmt="action")
|
|
3205
3210
|
else:
|
|
3206
3211
|
resp = cli.central.request(cli.cache.refresh_dev_db, dev_type="ap", **{**params, **default_params})
|
|
3207
3212
|
if resp.ok:
|
|
3208
3213
|
resp.output = [{"name": ap["name"], **rdict} for ap in resp.output for rdict in ap["radios"]]
|
|
3209
3214
|
|
|
3210
3215
|
if resp.ok:
|
|
3216
|
+
# We sort before sending data to renderer to keep groupings by AP name
|
|
3211
3217
|
if sort_by and resp.output and sort_by in resp.output[0].keys():
|
|
3212
3218
|
resp.output = list(sorted(resp.output, key=lambda ap: (ap["name"], ap[sort_by])))
|
|
3213
3219
|
else:
|
|
3214
3220
|
resp.output = list(sorted(resp.output, key=lambda ap: (ap["name"], ap["radio_name"])))
|
|
3221
|
+
|
|
3222
|
+
caption = _build_radio_caption(resp.output)
|
|
3215
3223
|
if status:
|
|
3216
3224
|
resp.output = list(filter(lambda radio: radio["status"] == status, resp.output))
|
|
3217
|
-
caption = _build_radio_caption(resp.output)
|
|
3218
3225
|
|
|
3219
|
-
cli.display_results(resp, tablefmt=tablefmt, title="Radio Details", reverse=reverse, outfile=outfile, pager=pager, caption=caption, cleaner=cleaner.show_radios)
|
|
3226
|
+
cli.display_results(resp, tablefmt=tablefmt, title="Radio Details", reverse=reverse, outfile=outfile, pager=pager, caption=caption, group_by="name", cleaner=cleaner.show_radios)
|
|
3220
3227
|
|
|
3221
3228
|
|
|
3222
3229
|
@app.command()
|
|
@@ -233,7 +233,7 @@ class ImportSites(RootModel):
|
|
|
233
233
|
|
|
234
234
|
@staticmethod
|
|
235
235
|
def _convert_site_key(_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
236
|
-
def auto_usa(data: Dict[str, str | int | float]) -> str:
|
|
236
|
+
def auto_usa(data: Dict[str, str | int | float]) -> str | None:
|
|
237
237
|
_country = data.get("country", "")
|
|
238
238
|
if _country.isdigit(): # Data from large customer had country as '1' for some sites
|
|
239
239
|
_country = ""
|
|
@@ -243,7 +243,7 @@ class ImportSites(RootModel):
|
|
|
243
243
|
if _country.upper() in ["USA", "US"]:
|
|
244
244
|
return "United States"
|
|
245
245
|
|
|
246
|
-
return _country
|
|
246
|
+
return _country or None
|
|
247
247
|
|
|
248
248
|
_data = [
|
|
249
249
|
{
|
|
@@ -170,12 +170,12 @@ def do_pretty(key: str, value: str) -> str:
|
|
|
170
170
|
value = "" if value is None else value # testing error on cop
|
|
171
171
|
return value if key != "status" else f'[b {color}]{value.title()}[/b {color}]'
|
|
172
172
|
|
|
173
|
-
def _do_subtables(data: List[dict], tablefmt: str = "rich") -> List[dict]:
|
|
173
|
+
def _do_subtables(data: List[dict], *, tablefmt: str = "rich") -> List[dict]:
|
|
174
174
|
"""Parse data and format any values that are dict, list, tuple
|
|
175
175
|
|
|
176
176
|
Args:
|
|
177
177
|
data (list): The data
|
|
178
|
-
tablefmt (str, optional): table format. Defaults to "rich"
|
|
178
|
+
tablefmt (str, optional): table format. Defaults to "rich"
|
|
179
179
|
|
|
180
180
|
Returns:
|
|
181
181
|
List[dict]: Original dict with any inner (dict/list/tuples)
|
|
@@ -250,11 +250,35 @@ def tabulate_output(outdata: List[dict]) -> tuple:
|
|
|
250
250
|
|
|
251
251
|
return raw_data, table_data
|
|
252
252
|
|
|
253
|
+
def build_rich_table_rows(data: List[Dict[str, Text | str]], table: Table, group_by: str) -> Table:
|
|
254
|
+
if not group_by:
|
|
255
|
+
[table.add_row(*list(in_dict.values())) for in_dict in data]
|
|
256
|
+
return table
|
|
257
|
+
|
|
258
|
+
if not isinstance(data, list) or group_by not in data[0]:
|
|
259
|
+
log.error(f"Error in render.do_group_by_table invalid type {type(data)} or {group_by} not found in header.")
|
|
260
|
+
[table.add_row(*list(in_dict.values())) for in_dict in data]
|
|
261
|
+
return table
|
|
262
|
+
|
|
263
|
+
field_idx = list(data[0].keys()).index(group_by)
|
|
264
|
+
this = "_start_"
|
|
265
|
+
for in_dict in data:
|
|
266
|
+
if in_dict[group_by] == this:
|
|
267
|
+
table.add_row(*[v if idx != field_idx else "" for idx, v in enumerate(in_dict.values())]) # only show value for first entry in group
|
|
268
|
+
else:
|
|
269
|
+
this = in_dict[group_by] # first entry in group
|
|
270
|
+
table.add_section()
|
|
271
|
+
table.add_row(*list(in_dict.values()))
|
|
272
|
+
|
|
273
|
+
return table
|
|
274
|
+
|
|
275
|
+
|
|
253
276
|
def rich_output(
|
|
254
277
|
outdata: List[dict],
|
|
255
278
|
title: str = None,
|
|
256
279
|
caption: str = None,
|
|
257
280
|
account: str = None,
|
|
281
|
+
group_by: str = None,
|
|
258
282
|
set_width_cols: dict = None,
|
|
259
283
|
full_cols: Union[List[str], str] = [],
|
|
260
284
|
fold_cols: Union[List[str], str] = [],
|
|
@@ -266,6 +290,7 @@ def rich_output(
|
|
|
266
290
|
title (str, optional): Table Title. Defaults to None.
|
|
267
291
|
caption (str, optional): Table Caption. Defaults to None.
|
|
268
292
|
account (str, optional): The account (displayed in caption if not the default). Defaults to None.
|
|
293
|
+
group_by (str, optional): Group output by the value of the provided field. Results in special formatting. Defaults to None.
|
|
269
294
|
set_width_cols (dict, optional): cols that need to be rendered with a specific width. Defaults to None.
|
|
270
295
|
full_cols (Union[List[str], str], optional): cols that should not be truncated. Defaults to [].
|
|
271
296
|
fold_cols (Union[List[str], str], optional): cols that can be folded (wrapped). Defaults to [].
|
|
@@ -324,7 +349,7 @@ def rich_output(
|
|
|
324
349
|
formatted = _do_subtables(outdata)
|
|
325
350
|
log.debug(f"render.rich_output.do_subtables took {time.perf_counter() - _start:.2f} to process {len(outdata)} records")
|
|
326
351
|
|
|
327
|
-
|
|
352
|
+
table = build_rich_table_rows(formatted, table=table, group_by=group_by)
|
|
328
353
|
|
|
329
354
|
if title:
|
|
330
355
|
table.title = f'[italic cornflower_blue]{constants.what_to_pretty(title)}'
|
|
@@ -352,6 +377,32 @@ def rich_output(
|
|
|
352
377
|
|
|
353
378
|
return outdata, outdata
|
|
354
379
|
|
|
380
|
+
|
|
381
|
+
def format_data_by_key(data: List[Dict[str, Any]], output_by_key: str) -> Dict[str, Any]:
|
|
382
|
+
# -- modify keys potentially formatted with \n for narrower rich output to format appropriate for json/yaml
|
|
383
|
+
data = utils.listify(data)
|
|
384
|
+
if isinstance(data[0], dict) and all([isinstance(k, str) for k in list(data[0].keys())]):
|
|
385
|
+
data = [{k.replace(" ", "_").replace("\n", "_"): v for k, v in d.items()} for d in data]
|
|
386
|
+
|
|
387
|
+
# -- convert List[dict] --> Dict[dev_name: dict] for yaml/json outputs unless output_dict_by_key is specified, then use the provided key(s) rather than name
|
|
388
|
+
if output_by_key and data and isinstance(data[0], dict):
|
|
389
|
+
if len(output_by_key) == 1 and "+" in output_by_key[0]:
|
|
390
|
+
found_keys = [k for k in output_by_key[0].split("+") if k in data[0]]
|
|
391
|
+
if len(found_keys) == len(output_by_key[0].split("+")):
|
|
392
|
+
data: Dict[dict] = {
|
|
393
|
+
f"{'-'.join([item[key] for key in found_keys])}": {k: v for k, v in item.items()} for item in data
|
|
394
|
+
}
|
|
395
|
+
else:
|
|
396
|
+
_output_key = [k for k in output_by_key if k in data[0]]
|
|
397
|
+
if _output_key:
|
|
398
|
+
_output_key = _output_key[0]
|
|
399
|
+
data: Dict[dict] = {
|
|
400
|
+
item[_output_key]: {k: v for k, v in item.items() if k != _output_key}
|
|
401
|
+
for item in data
|
|
402
|
+
}
|
|
403
|
+
return data
|
|
404
|
+
|
|
405
|
+
|
|
355
406
|
def output(
|
|
356
407
|
outdata: List[str] | List[Dict[str, Any]] | Dict[str, Any],
|
|
357
408
|
tablefmt: TableFormat = "rich", # "action" and "raw" are not sent through formatter, handled in clicommon.display_output
|
|
@@ -360,6 +411,7 @@ def output(
|
|
|
360
411
|
account: str = None,
|
|
361
412
|
config: Config = None,
|
|
362
413
|
output_by_key: str | List[str] = "name",
|
|
414
|
+
group_by: str = None,
|
|
363
415
|
set_width_cols: dict = None,
|
|
364
416
|
full_cols: Union[List[str], str] = [],
|
|
365
417
|
fold_cols: Union[List[str], str] = [],
|
|
@@ -368,6 +420,7 @@ def output(
|
|
|
368
420
|
raw_data = outdata
|
|
369
421
|
_lexer = table_data = None
|
|
370
422
|
|
|
423
|
+
# sanitize output for demos
|
|
371
424
|
if config and config.sanitize and raw_data and all(isinstance(x, dict) for x in raw_data):
|
|
372
425
|
outdata = [{k: d[k] if k not in REDACT else "--redacted--" for k in d} for d in raw_data]
|
|
373
426
|
|
|
@@ -376,27 +429,7 @@ def output(
|
|
|
376
429
|
tablefmt = "simple"
|
|
377
430
|
|
|
378
431
|
if tablefmt in ['json', 'yaml', 'yml']:
|
|
379
|
-
|
|
380
|
-
outdata = utils.listify(outdata)
|
|
381
|
-
if isinstance(outdata[0], dict) and all([isinstance(k, str) for k in list(outdata[0].keys())]):
|
|
382
|
-
outdata = [{k.replace(" ", "_").replace("\n", "_"): v for k, v in data.items()} for data in outdata]
|
|
383
|
-
|
|
384
|
-
# -- convert List[dict] --> Dict[dev_name: dict] for yaml/json outputs unless output_dict_by_key is specified, then use the provided key(s) rather than name
|
|
385
|
-
if output_by_key and outdata and isinstance(outdata[0], dict):
|
|
386
|
-
if len(output_by_key) == 1 and "+" in output_by_key[0]:
|
|
387
|
-
found_keys = [k for k in output_by_key[0].split("+") if k in outdata[0]]
|
|
388
|
-
if len(found_keys) == len(output_by_key[0].split("+")):
|
|
389
|
-
outdata: Dict[dict] = {
|
|
390
|
-
f"{'-'.join([item[key] for key in found_keys])}": {k: v for k, v in item.items()} for item in outdata
|
|
391
|
-
}
|
|
392
|
-
else:
|
|
393
|
-
_output_key = [k for k in output_by_key if k in outdata[0]]
|
|
394
|
-
if _output_key:
|
|
395
|
-
_output_key = _output_key[0]
|
|
396
|
-
outdata: Dict[dict] = {
|
|
397
|
-
item[_output_key]: {k: v for k, v in item.items() if k != _output_key}
|
|
398
|
-
for item in outdata
|
|
399
|
-
}
|
|
432
|
+
outdata = format_data_by_key(outdata, output_by_key)
|
|
400
433
|
|
|
401
434
|
if tablefmt == "json":
|
|
402
435
|
outdata = utils.unlistify(outdata)
|
|
@@ -445,8 +478,7 @@ def output(
|
|
|
445
478
|
table_data = rich_capture(table_data)
|
|
446
479
|
|
|
447
480
|
elif tablefmt == "rich":
|
|
448
|
-
raw_data, table_data = rich_output(outdata, title=title, caption=caption, account=account, set_width_cols=set_width_cols, full_cols=full_cols, fold_cols=fold_cols)
|
|
449
|
-
...
|
|
481
|
+
raw_data, table_data = rich_output(outdata, title=title, caption=caption, account=account, set_width_cols=set_width_cols, full_cols=full_cols, fold_cols=fold_cols, group_by=group_by)
|
|
450
482
|
|
|
451
483
|
elif tablefmt == "tabulate":
|
|
452
484
|
raw_data, table_data = tabulate_output(outdata)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "centralcli"
|
|
3
|
-
version = "8.2.
|
|
3
|
+
version = "8.2.4"
|
|
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 = [{"name" = "Wade Wells (Pack3tL0ss)","email" = "cencli@consolepi.com"}]
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from centralcli import config, log, cnx_client, utils, cleaner
|
|
2
|
-
from centralcli.typedefs import Method, StrOrURL
|
|
3
|
-
from centralcli.constants import lib_to_api, STRIP_KEYS
|
|
4
|
-
from centralcli.exceptions import CentralCliException
|
|
5
|
-
from centralcli.response import RateLimit, BatchRequest, LoggedRequests, Spinner, Response
|
|
6
|
-
from .pycentralv2.base import ArubaCentralNewBase
|
|
7
|
-
|
|
8
|
-
__all__ = [
|
|
9
|
-
config,
|
|
10
|
-
log,
|
|
11
|
-
cnx_client,
|
|
12
|
-
Method,
|
|
13
|
-
StrOrURL,
|
|
14
|
-
lib_to_api,
|
|
15
|
-
STRIP_KEYS,
|
|
16
|
-
cleaner,
|
|
17
|
-
utils,
|
|
18
|
-
CentralCliException,
|
|
19
|
-
RateLimit,
|
|
20
|
-
BatchRequest,
|
|
21
|
-
LoggedRequests,
|
|
22
|
-
Spinner,
|
|
23
|
-
Response,
|
|
24
|
-
ArubaCentralNewBase
|
|
25
|
-
]
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# from ...response import BatchRequest, Response, LoggedRequests, Spinner
|
|
2
|
-
# from ... import utils
|
|
3
|
-
# from ..pycentralv2.base import ArubaCentralNewBase
|
|
4
|
-
|
|
5
|
-
# __all__ = [
|
|
6
|
-
# BatchRequest,
|
|
7
|
-
# Response,
|
|
8
|
-
# LoggedRequests,
|
|
9
|
-
# Spinner,
|
|
10
|
-
# ArubaCentralNewBase,
|
|
11
|
-
# utils
|
|
12
|
-
# ]
|