centralcli 7.2.3__tar.gz → 7.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.
Files changed (64) hide show
  1. {centralcli-7.2.3 → centralcli-7.2.4}/PKG-INFO +1 -1
  2. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cache.py +24 -21
  3. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cleaner.py +1 -1
  4. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clicommon.py +16 -14
  5. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clikick.py +27 -12
  6. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishow.py +44 -4
  7. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clitest.py +1 -1
  8. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/models.py +1 -1
  9. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/response.py +2 -1
  10. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/strings.py +7 -0
  11. {centralcli-7.2.3 → centralcli-7.2.4}/pyproject.toml +1 -1
  12. {centralcli-7.2.3 → centralcli-7.2.4}/LICENSE +0 -0
  13. {centralcli-7.2.3 → centralcli-7.2.4}/README.md +0 -0
  14. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/__init__.py +0 -0
  15. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/boilerplate/README.md +0 -0
  16. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/boilerplate/_cnx_allcalls.py +0 -0
  17. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/boilerplate/allcalls.py +0 -0
  18. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/caas.py +0 -0
  19. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/central.py +0 -0
  20. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cli.py +0 -0
  21. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliadd.py +0 -0
  22. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliassign.py +0 -0
  23. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clibatch.py +0 -0
  24. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clicaas.py +0 -0
  25. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clicheck.py +0 -0
  26. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliclone.py +0 -0
  27. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clidel.py +0 -0
  28. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clidelfirmware.py +0 -0
  29. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliexport.py +0 -0
  30. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clioptions.py +0 -0
  31. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clirefresh.py +0 -0
  32. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clirename.py +0 -0
  33. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliset.py +0 -0
  34. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clisetfirmware.py +0 -0
  35. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowaudit.py +0 -0
  36. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowbandwidth.py +0 -0
  37. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowbranch.py +0 -0
  38. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowcloudauth.py +0 -0
  39. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowfirmware.py +0 -0
  40. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowmpsk.py +0 -0
  41. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowospf.py +0 -0
  42. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowoverlay.py +0 -0
  43. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowtshoot.py +0 -0
  44. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clishowwids.py +0 -0
  45. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/clitshoot.py +0 -0
  46. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliunassign.py +0 -0
  47. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliupdate.py +0 -0
  48. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/cliupgrade.py +0 -0
  49. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/config.py +0 -0
  50. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/constants.py +0 -0
  51. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/exceptions.py +0 -0
  52. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/logger.py +0 -0
  53. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/objects.py +0 -0
  54. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/render.py +0 -0
  55. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/setup.py +0 -0
  56. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/static/favicon.ico +0 -0
  57. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/typedefs.py +0 -0
  58. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/utils.py +0 -0
  59. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/vendored/csvlexer/__init__.py +0 -0
  60. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/vendored/csvlexer/csv.py +0 -0
  61. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/vscodeargs.py +0 -0
  62. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/wh2snow.py +0 -0
  63. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/wh_proxy.py +0 -0
  64. {centralcli-7.2.3 → centralcli-7.2.4}/centralcli/wh_proxy_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: centralcli
3
- Version: 7.2.3
3
+ Version: 7.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
  Home-page: https://github.com/Pack3tL0ss/central-api-cli
6
6
  License: MIT
@@ -3227,27 +3227,30 @@ class Cache:
3227
3227
 
3228
3228
 
3229
3229
  # no match found initiate cache update
3230
- if retry and not match and ((dev_type and not cache_updated) or self.responses.dev is None):
3231
- dev_type_sfx = "" if not dev_type else f" [grey42 italic](Device Type: {utils.unlistify(dev_type)})[/]"
3232
- econsole.print(f"[dark_orange3]:warning:[/] [bright_red]No Match found[/] for [cyan]{query_str}[/]{dev_type_sfx}.")
3233
- if FUZZ:
3234
- if dev_type:
3235
- fuzz_match, fuzz_confidence = process.extract(query_str, [d["name"] for d in self.devices if d["type"] in dev_type], limit=1)[0]
3236
- else:
3237
- fuzz_match, fuzz_confidence = process.extract(query_str, [d["name"] for d in self.devices], limit=1)[0]
3238
- confirm_str = render.rich_capture(f"Did you mean [green3]{fuzz_match}[/]?")
3239
- if fuzz_confidence >= 70 and typer.confirm(confirm_str):
3240
- match = self.DevDB.search(self.Q.name == fuzz_match)
3241
- if not match:
3242
- kwargs = {"dev_db": True}
3243
- if include_inventory:
3244
- _word = " & Inventory "
3245
- kwargs["inv_db"] = True
3246
- else:
3247
- _word = " "
3248
- econsole.print(f":arrows_clockwise: Updating Device{_word}Cache.")
3249
- self.check_fresh(refresh=True, dev_type=dev_type, **kwargs )
3250
- cache_updated = True # Need this for scenario when dev_type is the only thing refreshed, as that does not update self.responses.dev
3230
+ if retry and not match and self.responses.dev is None:
3231
+ if dev_type and cache_updated:
3232
+ ... # self.responses.dev is not currently updated if dev_type provided, but cache update may have already occured in this session.
3233
+ else:
3234
+ dev_type_sfx = "" if not dev_type else f" [grey42 italic](Device Type: {utils.unlistify(dev_type)})[/]"
3235
+ econsole.print(f"[dark_orange3]:warning:[/] [bright_red]No Match found[/] for [cyan]{query_str}[/]{dev_type_sfx}.")
3236
+ if FUZZ:
3237
+ if dev_type:
3238
+ fuzz_match, fuzz_confidence = process.extract(query_str, [d["name"] for d in self.devices if d["type"] in dev_type], limit=1)[0]
3239
+ else:
3240
+ fuzz_match, fuzz_confidence = process.extract(query_str, [d["name"] for d in self.devices], limit=1)[0]
3241
+ confirm_str = render.rich_capture(f"Did you mean [green3]{fuzz_match}[/]?")
3242
+ if fuzz_confidence >= 70 and typer.confirm(confirm_str):
3243
+ match = self.DevDB.search(self.Q.name == fuzz_match)
3244
+ if not match:
3245
+ kwargs = {"dev_db": True}
3246
+ if include_inventory:
3247
+ _word = " & Inventory "
3248
+ kwargs["inv_db"] = True
3249
+ else:
3250
+ _word = " "
3251
+ econsole.print(f":arrows_clockwise: Updating Device{_word}Cache.")
3252
+ self.check_fresh(refresh=True, dev_type=dev_type, **kwargs )
3253
+ cache_updated = True # Need this for scenario when dev_type is the only thing refreshed, as that does not update self.responses.dev
3251
3254
 
3252
3255
  if match:
3253
3256
  match = [Model(dev) for dev in match]
@@ -1626,7 +1626,7 @@ def show_interfaces(data: List[dict] | dict, verbosity: int = 0, dev_type: DevTy
1626
1626
  key_order = verbosity_keys[verbosity]
1627
1627
  # send all key/value pairs through formatters
1628
1628
  data = [
1629
- dict(short_value(k, d.get(k),) for k in key_order) for d in data
1629
+ dict(short_value(k, d[k],) for k in key_order if k in d) for d in data
1630
1630
  ]
1631
1631
  else:
1632
1632
  key_order = [*key_order, *data[-1].keys()]
@@ -383,8 +383,7 @@ class CLICommon:
383
383
  else:
384
384
  return default
385
385
 
386
- @staticmethod
387
- def write_file(outfile: Path, outdata: str) -> None:
386
+ def write_file(self, outfile: Path, outdata: str) -> None:
388
387
  """Output data to file
389
388
 
390
389
  Args:
@@ -407,19 +406,22 @@ class CLICommon:
407
406
 
408
407
  print(f"\n[cyan]Writing output to {outfile}... ", end="")
409
408
 
410
- out_msg = None
411
- try:
412
- if isinstance(outdata, (dict, list)):
413
- outdata = json.dumps(outdata, indent=4)
414
- outfile.write_text(outdata) # typer.unstyle(outdata) also works
415
- except Exception as e:
416
- outfile.write_text(f"{outdata}")
417
- out_msg = f"Error ({e.__class__.__name__}) occurred during attempt to output to file. " \
418
- "Used simple string conversion"
409
+ if not outfile.parent.is_dir():
410
+ self.econsole.print(f"[red]Directory Not Found[/]\n[dark_orange3]:warning:[/] Unable to write output to [cyan]{outfile.name}[/].\nDirectory [cyan]{str(outfile.parent.absolute())}[/] [red]does not exist[/].")
411
+ else:
412
+ out_msg = None
413
+ try:
414
+ if isinstance(outdata, (dict, list)):
415
+ outdata = json.dumps(outdata, indent=4)
416
+ outfile.write_text(outdata) # typer.unstyle(outdata) also works
417
+ except Exception as e:
418
+ outfile.write_text(f"{outdata}")
419
+ out_msg = f"Error ({e.__class__.__name__}) occurred during attempt to output to file. " \
420
+ "Used simple string conversion"
419
421
 
420
- print("[italic green]Done")
421
- if out_msg:
422
- log.warning(out_msg, show=True)
422
+ print("[italic green]Done")
423
+ if out_msg:
424
+ log.warning(out_msg, show=True)
423
425
 
424
426
  @staticmethod
425
427
  def exit(msg: str = None, code: int = 1, emoji: bool = True) -> None:
@@ -8,7 +8,6 @@ from rich import print
8
8
 
9
9
 
10
10
  # Detect if called from pypi installed package or via cloned github repo (development)
11
- # TODO should be able to do this in __init__
12
11
  try:
13
12
  from centralcli import cli
14
13
  except (ImportError, ModuleNotFoundError) as e:
@@ -19,16 +18,18 @@ except (ImportError, ModuleNotFoundError) as e:
19
18
  else:
20
19
  print(pkg_dir.parts)
21
20
  raise e
22
- from centralcli.constants import IdenMetaVars
23
21
 
24
- iden = IdenMetaVars()
22
+ from centralcli.constants import iden_meta
23
+ from .cache import CacheClient
24
+ from .models import Clients
25
+
25
26
  app = typer.Typer()
26
27
 
27
28
  @app.command(short_help="Disconnect all WLAN clients from an AP optionally for a specific SSID",)
28
29
  def all(
29
30
  device: str = typer.Argument(
30
31
  ...,
31
- metavar=iden.dev,
32
+ metavar=iden_meta.dev,
32
33
  help="The AP to disconnect clients from",
33
34
  autocompletion=cli.cache.dev_ap_completion,
34
35
  show_default=False,
@@ -46,7 +47,7 @@ def all(
46
47
  dev = cli.cache.get_dev_identifier(device)
47
48
  _ssid_msg = "" if not ssid else f" on SSID [cyan]{ssid}[/]"
48
49
  print(f'Kick [bright_red]ALL[/] users connected to [cyan]{dev.name}[/]{_ssid_msg}')
49
- if yes or typer.confirm("\nproceed?", abort=True):
50
+ if cli.confirm(yes):
50
51
  resp = cli.central.request(
51
52
  cli.central.kick_users,
52
53
  dev.serial,
@@ -56,12 +57,10 @@ def all(
56
57
  cli.display_results(resp, tablefmt="action")
57
58
 
58
59
 
59
- # TODO rather than drop option have cache remove users with last_connected > 30 days
60
60
  @app.command(short_help="Disconnect a WLAN client",)
61
61
  def client(
62
- client: str = typer.Argument(..., metavar=iden.client, autocompletion=cli.cache.client_completion, show_default=False),
62
+ client: str = typer.Argument(..., metavar=iden_meta.client, autocompletion=cli.cache.client_completion, show_default=False),
63
63
  refresh: bool = typer.Option(False, "--refresh", "-R", help="Cache is used to determine what AP the client is connected to, which could be [red]stale[/]. This forces a cache update."),
64
- drop: bool = typer.Option(False, "--drop", "-D", help="(implies -R): Drop all users from existing cache, then refresh. By default any user that has ever connected is retained in the cache."),
65
64
  yes: bool = cli.options.yes,
66
65
  debug: bool = cli.options.debug,
67
66
  default: bool = cli.options.default,
@@ -75,15 +74,31 @@ def client(
75
74
 
76
75
  The [cyan]-R[/] flag can be used to force a cache refresh prior to performing the disconnect.
77
76
  """
78
- if refresh or drop:
79
- resp = cli.central.request(cli.cache.refresh_client_db, "wireless", truncate=drop)
77
+ if refresh:
78
+ resp = cli.central.request(cli.cache.refresh_client_db, client_type="wireless")
80
79
  if not resp:
81
80
  cli.display_results(resp, exit_on_fail=True)
82
81
 
83
- client = cli.cache.get_client_identifier(client, exit_on_fail=True)
82
+ client: CacheClient = cli.cache.get_client_identifier(client, exit_on_fail=True)
83
+ if not client.last_connected:
84
+ if refresh:
85
+ cli.exit(f"Client {client} is not connected.")
86
+ else:
87
+ client_resp = cli.central.request(cli.cache.refresh_client_db, mac=client.mac)
88
+ if not client_resp:
89
+ cli.econsole.print(f"client {client} is not online according to cache, Failure occured attempting to fetch client details from API.")
90
+ cli.display_results(client_resp, exit_on_fail=True)
91
+
92
+ _clients = [CacheClient(c) for c in Clients(client_resp.output)]
93
+ online_client = [c for c in _clients if c.last_connected is not None]
94
+ if online_client:
95
+ client = online_client[-1]
96
+ else:
97
+ client = _clients[-1]
98
+ cli.exit(f"Client {client} is not online: Failure Stage: {client_resp.output[-1].get('failure_stage', '')}, Reason: {client_resp.output[-1].get('failure_reason', '')}")
84
99
 
85
100
  print(f'Kick client [cyan]{client.name}[/], currently connected to [cyan]{client.connected_name}[/]')
86
- if yes or typer.confirm("\nProceed?", abort=True):
101
+ if cli.confirm(yes):
87
102
  resp = cli.central.request(
88
103
  cli.central.kick_users,
89
104
  client.connected_serial,
@@ -10,6 +10,8 @@ import os
10
10
  from datetime import datetime
11
11
  from typing import List, Iterable, Literal, Dict, Any, Tuple, TYPE_CHECKING
12
12
  from pathlib import Path
13
+ import getpass
14
+ from jinja2 import Template
13
15
  from rich import print
14
16
  from rich.console import Console
15
17
 
@@ -48,6 +50,7 @@ from centralcli.constants import (
48
50
  )
49
51
  from centralcli.cache import CentralObject
50
52
  from centralcli.objects import DateTime
53
+ from .strings import cron_weekly
51
54
 
52
55
  if TYPE_CHECKING:
53
56
  from .cache import CacheSite, CacheGroup, CacheLabel, CacheDevice
@@ -163,7 +166,7 @@ def _build_device_caption(resp: Response, *, inventory: bool = False, dev_type:
163
166
 
164
167
  # Put together counts caption string
165
168
  if status:
166
- _cnt_str = ", ".join([f'[{"bright_green" if status.lower() == "up" else "red"}]{t}[/]: [cyan]{status_by_type[t]["total"]}[/]' for t in status_by_type])
169
+ _cnt_str = ", ".join([f'[{"bright_green" if status.lower() == "up" else "red"}]{status.capitalize()} {t if t != "ap" else "APs"}[/]: [cyan]{status_by_type[t]["total"]}[/]' for t in status_by_type])
167
170
  elif inventory:
168
171
  _cnt_str = f"Total in inventory: [cyan]{len(resp.output)}[/], "
169
172
  _cnt_str = _cnt_str + ", ".join(
@@ -558,7 +561,7 @@ def aps(
558
561
  if up and down:
559
562
  ... # They used both flags. ignore
560
563
  elif up or down:
561
- status = "Down" if down else "Up"
564
+ status = "down" if down else "up"
562
565
 
563
566
  show_devices(
564
567
  aps, dev_type="ap", include_inventory=with_inv, verbosity=verbose, outfile=outfile, update_cache=update_cache, group=group, site=site, label=label, status=status,
@@ -2009,6 +2012,7 @@ def wlans(
2009
2012
  "calculate_client_count": True,
2010
2013
  }
2011
2014
 
2015
+ # TODO specifying WLAN name ... is ignored if verbose
2012
2016
  tablefmt = cli.get_format(do_json=do_json, do_yaml=do_yaml, do_csv=do_csv, do_table=do_table, default="rich")
2013
2017
  if group: # Specifying the group implies verbose (same # of API calls either way.)
2014
2018
  resp = central.request(central.get_full_wlan_list, group)
@@ -2032,7 +2036,7 @@ def wlans(
2032
2036
  else:
2033
2037
  resp = central.request(central.get_wlans, **params)
2034
2038
  caption = None
2035
- if resp:
2039
+ if resp and not name:
2036
2040
  caption = [f'[green]{len(resp.output)}[/] SSIDs, [green]{sum([wlan.get("client_count", 0) for wlan in resp.output])}[/] Wireless Clients.']
2037
2041
  caption += ["Summary Output, Specify the group ([cyan]--group GROUP[/])", "or use the verbose flag ([cyan]`-v`[/]) for additional details"]
2038
2042
  cli.display_results(resp, tablefmt=tablefmt, title=title, caption=caption, pager=pager, outfile=outfile, sort_by=sort_by, reverse=reverse, cleaner=cleaner.get_wlans)
@@ -2875,7 +2879,7 @@ def portals(
2875
2879
  # TODO add sort_by completion, portal completion
2876
2880
  @app.command()
2877
2881
  def guests(
2878
- portal: str = typer.Argument(..., help="portal name", show_default=False,),
2882
+ portal: str = typer.Argument(..., help="portal name", autocompletion=cli.cache.portal_completion, show_default=False,),
2879
2883
  sort_by: str = cli.options.sort_by,
2880
2884
  reverse: bool = cli.options.reverse,
2881
2885
  do_json: bool = cli.options.do_json,
@@ -3017,6 +3021,42 @@ def version(
3017
3021
  """
3018
3022
  cli.version_callback()
3019
3023
 
3024
+ @app.command(hidden=os.name != "posix")
3025
+ def cron(
3026
+ accounts: List[str] = typer.Argument(None,),
3027
+ ) -> None:
3028
+ """Show contents of cron file that can be used to automate token refresh weekly.
3029
+
3030
+ This will keep the tokens valid, even if cencli is not used.
3031
+ """
3032
+ if os.name != "posix":
3033
+ cli.econsole.print("This command is currently only supported on Linux using cron. It is possible to do the same via Windows Task Scheduler. Showing Linux cron.weekly output for reference.")
3034
+
3035
+ user = getpass.getuser()
3036
+ exec_path = sys.argv[0]
3037
+ py_path = sys.executable
3038
+
3039
+ config_data = {
3040
+ "user": user,
3041
+ "py_path": py_path,
3042
+ "exec_path": exec_path,
3043
+ "accounts": "" if not accounts else " ".join(accounts)
3044
+ }
3045
+
3046
+ template = Template(cron_weekly)
3047
+ config_out = template.render(config_data)
3048
+
3049
+ cli.econsole.rule("/etc/cron.weekly/cencli file contents")
3050
+ cli.console.print(config_out)
3051
+ cli.econsole.rule()
3052
+
3053
+ cli.econsole.print(
3054
+ "Place the above contents into a file: /etc/cron.weekly/cencli [grey42 italic](requires sudo)[/]\n"
3055
+ f"Alternatively you can pipe the output directly [cyan]cencli show cron {'' if not accounts else ' '.join(accounts)} | sudo tee /etc/cron.weekly/cencli[/]"
3056
+ "\nThen make it executable: [cyan]sudo chmod +x /etc/cron.weekly/cencli[/]"
3057
+ "\n\n[cyan]cencli refresh token[/] [dark_olive_green2 italic]command will always update the tokens for the default workspace (that's the -d flag)[/]"
3058
+ )
3059
+
3020
3060
 
3021
3061
  def _get_cencli_config() -> None:
3022
3062
  try:
@@ -167,7 +167,7 @@ def method(
167
167
  resp = _check_bool_to_str(args, kwargs, resp_str=str(e))
168
168
 
169
169
  attrs = {
170
- k: v for k, v in resp.__dict__.items() if k not in ["output", "raw"] and (log.DEBUG or not k.startswith("_"))
170
+ k: v for k, v in resp.__dict__.items() if k not in ["output", "raw", "data_key"] and (log.DEBUG or not k.startswith("_"))
171
171
  }
172
172
 
173
173
  req = (
@@ -411,7 +411,7 @@ class Client(BaseModel):
411
411
  @classmethod
412
412
  def pretty_dt(cls, dt: datetime) -> DateTime:
413
413
  if dt is None: # TODO all with potential for there not to be a value need this
414
- return None
414
+ return DateTime(None, "timediff") # resolves PydanticSerializationError when model_dump_json is called on client with None for last_connected (Failed)
415
415
 
416
416
  return DateTime(dt.timestamp(), "timediff")
417
417
 
@@ -383,7 +383,8 @@ class Response:
383
383
  r = r.replace("message: ", "").replace(self.output["message"], f'[red italic]{self.output["message"]}[/]')
384
384
 
385
385
  r = r.replace("failed:", "[red]failed[/]:").replace("FAILED", "[red]FAILED[/red]").replace("failed_devices", "[red]failed_devices[/]").replace("INVALID", "[red]INVALID[/]")
386
- r = r.replace("SUCCESS", "[bright_green]SUCCESS[/]").replace("Success", "[bright_green]Success[/]").replace("success", "[bright_green]success[/]").replace("succeeded_devices", "[bright_green]succeeded_devices[/]")
386
+ r = r.replace("SUCCESS", "[bright_green]SUCCESS[/]").replace("Success", "[bright_green]Success[/]").replace("Success[/]fully", "Successfully[/]")
387
+ r = r.replace("Success", "[bright_green]Success[/]").replace("success", "[bright_green]success[/]").replace("succeeded_devices", "[bright_green]succeeded_devices[/]")
387
388
  r = r.replace("invalid_device", "[red]invalid_device[/]").replace("blocked_device", "[red]blocked_device[/red]").replace("ATHENA_ERROR_DEVICE_ALREADY_EXIST", "[italic dark_orange3]Device already exists[/]")
388
389
 
389
390
  # sanitize sensitive data for demos
@@ -589,3 +589,10 @@ class ImportExamples:
589
589
  if key not in self.__dict__.keys():
590
590
  log.error(f"An attempt was made to get {key} attr from ImportExamples which is not defined.")
591
591
  return f":warning: [bright_red]Error[/] no str defined for [cyan]ImportExamples.{key}[/]"
592
+
593
+ cron_weekly = """#!/usr/bin/env bash
594
+
595
+ /bin/su -c "{{py_path}} {{exec_path}} refresh token -d {{accounts}}" {{user}} &&
596
+ logger -t centralcli "Token Refreshed via cron" ||
597
+ logger -t centralcli "Token Refresh returned error"
598
+ """
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "centralcli"
3
- version = "7.2.3"
3
+ version = "7.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 = ["Wade Wells (Pack3tL0ss) <wade@consolepi.org>"]
File without changes
File without changes
File without changes