exosphere-cli 0.9.9__py3-none-any.whl
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.
- exosphere/__init__.py +14 -0
- exosphere/cli.py +129 -0
- exosphere/commands/__init__.py +0 -0
- exosphere/commands/config.py +163 -0
- exosphere/commands/host.py +317 -0
- exosphere/commands/inventory.py +518 -0
- exosphere/commands/ui.py +31 -0
- exosphere/compat/__init__.py +0 -0
- exosphere/compat/win32readline.py +134 -0
- exosphere/config.py +281 -0
- exosphere/context.py +4 -0
- exosphere/data.py +31 -0
- exosphere/database.py +33 -0
- exosphere/errors.py +27 -0
- exosphere/inventory.py +363 -0
- exosphere/main.py +172 -0
- exosphere/objects.py +361 -0
- exosphere/providers/__init__.py +11 -0
- exosphere/providers/api.py +64 -0
- exosphere/providers/debian.py +144 -0
- exosphere/providers/factory.py +29 -0
- exosphere/providers/freebsd.py +183 -0
- exosphere/providers/redhat.py +226 -0
- exosphere/setup/__init__.py +0 -0
- exosphere/setup/detect.py +236 -0
- exosphere/ui/__init__.py +0 -0
- exosphere/ui/app.py +55 -0
- exosphere/ui/dashboard.py +150 -0
- exosphere/ui/elements.py +183 -0
- exosphere/ui/inventory.py +391 -0
- exosphere/ui/logs.py +110 -0
- exosphere/ui/style.tcss +160 -0
- exosphere_cli-0.9.9.dist-info/METADATA +104 -0
- exosphere_cli-0.9.9.dist-info/RECORD +37 -0
- exosphere_cli-0.9.9.dist-info/WHEEL +4 -0
- exosphere_cli-0.9.9.dist-info/entry_points.txt +3 -0
- exosphere_cli-0.9.9.dist-info/licenses/LICENSE +21 -0
exosphere/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
from .config import Configuration
|
|
4
|
+
|
|
5
|
+
# Global Instances: configuration and GlobalState
|
|
6
|
+
# These are set at runtime and should be used as singletons
|
|
7
|
+
# to hold the global state and configuration.
|
|
8
|
+
|
|
9
|
+
app_config = Configuration() # Has default values out of the box
|
|
10
|
+
|
|
11
|
+
# Current software version, imported from pyproject metadata
|
|
12
|
+
__version__ = importlib.metadata.version("exosphere_cli")
|
|
13
|
+
|
|
14
|
+
__all__ = ["__version__", "app_config"]
|
exosphere/cli.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
# ------------------win32 readline monkeypatch---------------------
|
|
6
|
+
if sys.platform == "win32":
|
|
7
|
+
try:
|
|
8
|
+
# On windows, we use a wrapper module for pyreadline3 in order
|
|
9
|
+
# to provide readline compatibility.
|
|
10
|
+
from exosphere.compat import win32readline as readline
|
|
11
|
+
|
|
12
|
+
# This needs monkeypatched in order for click_shell to make use
|
|
13
|
+
# of it instead of its internal, broken, legacy pyreadline.
|
|
14
|
+
sys.modules["readline"] = readline
|
|
15
|
+
except ImportError:
|
|
16
|
+
sys.stderr.write(
|
|
17
|
+
"Warning: pyreadline3 not found. "
|
|
18
|
+
"Interactive shell may not enable all features.\n"
|
|
19
|
+
)
|
|
20
|
+
# -----------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
from click_shell import make_click_shell
|
|
23
|
+
from rich import print
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
from typer import Argument, Context, Exit, Typer
|
|
26
|
+
|
|
27
|
+
from exosphere import __version__
|
|
28
|
+
from exosphere.commands import config, host, inventory, ui
|
|
29
|
+
|
|
30
|
+
banner = f"""[turquoise4]
|
|
31
|
+
▗▖
|
|
32
|
+
▐▌
|
|
33
|
+
▟█▙ ▝█ █▘ ▟█▙ ▗▟██▖▐▙█▙ ▐▙██▖ ▟█▙ █▟█▌ ▟█▙
|
|
34
|
+
▐▙▄▟▌ ▐█▌ ▐▛ ▜▌▐▙▄▖▘▐▛ ▜▌▐▛ ▐▌▐▙▄▟▌ █▘ ▐▙▄▟▌
|
|
35
|
+
▐▛▀▀▘ ▗█▖ ▐▌ ▐▌ ▀▀█▖▐▌ ▐▌▐▌ ▐▌▐▛▀▀▘ █ ▐▛▀▀▘
|
|
36
|
+
▝█▄▄▌ ▟▀▙ ▝█▄█▘▐▄▄▟▌▐█▄█▘▐▌ ▐▌▝█▄▄▌ █ ▝█▄▄▌
|
|
37
|
+
▝▀▀ ▝▀ ▀▘ ▝▀▘ ▀▀▀ ▐▌▀▘ ▝▘ ▝▘ ▝▀▀ ▀ ▝▀▀
|
|
38
|
+
▐▌ [green]v{__version__}[/green][/turquoise4]
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
app = Typer(
|
|
42
|
+
no_args_is_help=False,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Setup commands from modules
|
|
46
|
+
app.add_typer(inventory.app, name="inventory")
|
|
47
|
+
app.add_typer(host.app, name="host")
|
|
48
|
+
app.add_typer(ui.app, name="ui")
|
|
49
|
+
app.add_typer(config.app, name="config")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command(hidden=True)
|
|
53
|
+
def help(ctx: Context, command: Annotated[str | None, Argument()] = None):
|
|
54
|
+
"""
|
|
55
|
+
Help for interactive REPL use
|
|
56
|
+
|
|
57
|
+
Provides help for the root REPL command when used interactively,
|
|
58
|
+
in a way that is friendler for that specific context.
|
|
59
|
+
If a command is specified, it will show help for that command.
|
|
60
|
+
|
|
61
|
+
This only applies when in the interactive REPL, commands (including
|
|
62
|
+
the root 'exosphere' program) will use the standard Typer help
|
|
63
|
+
system when invoked from the command line or non-interactively.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
msg = "\nUse '<command> --help' or 'help <command>' for help on a specific command."
|
|
67
|
+
|
|
68
|
+
# Show root help if no command is specified
|
|
69
|
+
if not command:
|
|
70
|
+
if ctx.parent and getattr(ctx.parent, "command", None):
|
|
71
|
+
subcommands = getattr(ctx.parent.command, "commands", {})
|
|
72
|
+
lines = []
|
|
73
|
+
for name, cmd in subcommands.items():
|
|
74
|
+
if cmd.hidden:
|
|
75
|
+
continue
|
|
76
|
+
lines.append(
|
|
77
|
+
f"[cyan]{name:<11}[/cyan] {cmd.help or 'No description available.'}"
|
|
78
|
+
)
|
|
79
|
+
content = "\n".join(lines)
|
|
80
|
+
panel = Panel.fit(
|
|
81
|
+
content,
|
|
82
|
+
title="Commands",
|
|
83
|
+
title_align="left",
|
|
84
|
+
)
|
|
85
|
+
print("\nAvailable modules during interactive use:\n")
|
|
86
|
+
print(panel)
|
|
87
|
+
print(msg)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Show command help if one is specified
|
|
91
|
+
subcommand = None
|
|
92
|
+
if ctx.parent and getattr(ctx.parent, "command", None):
|
|
93
|
+
subcommands = getattr(ctx.parent.command, "commands", None)
|
|
94
|
+
subcommand = subcommands.get(command) if subcommands else None
|
|
95
|
+
if subcommand:
|
|
96
|
+
subcommand.get_help(ctx)
|
|
97
|
+
print(f"\nUse '{str(subcommand.name)} <command> --help' for more details.")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Fall through for unknown commands
|
|
101
|
+
print(f"[red]Unkown command '{command}'[/red]")
|
|
102
|
+
print(msg)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.callback(invoke_without_command=True)
|
|
106
|
+
def cli(ctx: Context) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Exosphere CLI
|
|
109
|
+
|
|
110
|
+
The main command-line interface for Exosphere.
|
|
111
|
+
It provides a REPL interface for interactive use as a prompt, but can
|
|
112
|
+
also be used to run commands directly from the command line.
|
|
113
|
+
|
|
114
|
+
Run without arguments to start the interactive mode.
|
|
115
|
+
"""
|
|
116
|
+
if ctx.invoked_subcommand is None:
|
|
117
|
+
logger = logging.getLogger(__name__)
|
|
118
|
+
logger.info("Starting Exosphere REPL interface")
|
|
119
|
+
|
|
120
|
+
# Print the banner
|
|
121
|
+
print(banner)
|
|
122
|
+
|
|
123
|
+
# Start interactive REPL
|
|
124
|
+
repl = make_click_shell(
|
|
125
|
+
ctx,
|
|
126
|
+
prompt="exosphere> ",
|
|
127
|
+
)
|
|
128
|
+
repl.cmdloop()
|
|
129
|
+
Exit(0)
|
|
File without changes
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.pretty import Pretty
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from exosphere import app_config, context
|
|
10
|
+
from exosphere.config import Configuration
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
help="Runtime Configuration Commands",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
err_console = Console(stderr=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def show(
|
|
23
|
+
option: Annotated[
|
|
24
|
+
str | None,
|
|
25
|
+
typer.Argument(help="Name of the option to show. All if not specified."),
|
|
26
|
+
] = None,
|
|
27
|
+
full: Annotated[
|
|
28
|
+
bool,
|
|
29
|
+
typer.Option(
|
|
30
|
+
"--full",
|
|
31
|
+
"-f",
|
|
32
|
+
help="Show full configuration structure, including inventory.",
|
|
33
|
+
),
|
|
34
|
+
] = False,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Show the current configuration.
|
|
38
|
+
|
|
39
|
+
Displays the current configuration options, or the value of a specific option
|
|
40
|
+
if specified.
|
|
41
|
+
|
|
42
|
+
If `--full` is specified, it will show the entire configuration structure,
|
|
43
|
+
including the inventory, beyond just the "options" section.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if full:
|
|
47
|
+
if option:
|
|
48
|
+
err_console.print(
|
|
49
|
+
"[yellow]Full configuration requested, ignoring option name.[/yellow]"
|
|
50
|
+
)
|
|
51
|
+
console.print(Pretty(app_config, expand_all=True, max_depth=None))
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if option:
|
|
55
|
+
if option in app_config["options"]:
|
|
56
|
+
console.print(app_config["options"][option])
|
|
57
|
+
else:
|
|
58
|
+
err_console.print(
|
|
59
|
+
f"[red]Option '{option}' not found in configuration.[/red]"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
console.print(Pretty(app_config["options"], expand_all=True))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def source(
|
|
69
|
+
env: Annotated[
|
|
70
|
+
bool,
|
|
71
|
+
typer.Option(
|
|
72
|
+
help="Show environment variables that affect the configuration.",
|
|
73
|
+
),
|
|
74
|
+
] = True,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Show the configuration source, where it was loaded from.
|
|
78
|
+
|
|
79
|
+
Displays the path of the configuration file loaded, if any, and
|
|
80
|
+
any environment variables that affect the configuration.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if context.confpath:
|
|
84
|
+
console.print(f"{context.confpath}")
|
|
85
|
+
else:
|
|
86
|
+
err_console.print("No configuration loaded, using defaults.")
|
|
87
|
+
|
|
88
|
+
if not env:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
env_lines: list[str] = []
|
|
92
|
+
|
|
93
|
+
prefix = "EXOSPHERE_OPTIONS_"
|
|
94
|
+
|
|
95
|
+
for key, value in os.environ.items():
|
|
96
|
+
if (
|
|
97
|
+
key.startswith(prefix)
|
|
98
|
+
and key.removeprefix(prefix).lower() in app_config["options"]
|
|
99
|
+
):
|
|
100
|
+
env_lines.append(f"{key}={value}")
|
|
101
|
+
|
|
102
|
+
if env_lines:
|
|
103
|
+
console.print()
|
|
104
|
+
console.print("Environment variable overrides:\n")
|
|
105
|
+
for line in env_lines:
|
|
106
|
+
console.print(f" {line}")
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command()
|
|
111
|
+
def diff(
|
|
112
|
+
full: Annotated[
|
|
113
|
+
bool,
|
|
114
|
+
typer.Option(
|
|
115
|
+
"--full",
|
|
116
|
+
"-f",
|
|
117
|
+
help="Show full configuration diff, including unmodified options.",
|
|
118
|
+
),
|
|
119
|
+
] = False,
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
Show the differences between the current configuration and the defaults.
|
|
123
|
+
|
|
124
|
+
Exosphere follows convention over configuration, so your configuration
|
|
125
|
+
file can exclusively contain the options you want to change.
|
|
126
|
+
|
|
127
|
+
This command allows you to see exactly what has been changed, optionally
|
|
128
|
+
in its context, using the `--full` option.
|
|
129
|
+
|
|
130
|
+
For a full config dump, use the `show` command instead.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
default_config = Configuration.DEFAULTS["options"]
|
|
134
|
+
current_config = app_config["options"]
|
|
135
|
+
|
|
136
|
+
for key in set(default_config) | set(current_config):
|
|
137
|
+
if default_config.get(key, None) != current_config.get(key, None):
|
|
138
|
+
break
|
|
139
|
+
else:
|
|
140
|
+
console.print("No differences found between current and default configuration.")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
lines = []
|
|
144
|
+
for key in sorted(set(default_config) | set(current_config)):
|
|
145
|
+
default_value = default_config.get(key, None)
|
|
146
|
+
current_value = current_config.get(key, None)
|
|
147
|
+
|
|
148
|
+
line: Text | None
|
|
149
|
+
|
|
150
|
+
if default_value != current_value:
|
|
151
|
+
line = Text(f"{key!r}: {current_value!r},", style="bold green")
|
|
152
|
+
line.append(f" # default: {default_value!r}", style="yellow")
|
|
153
|
+
else:
|
|
154
|
+
line = Text(f"{key!r}: {current_value!r},", style="dim") if full else None
|
|
155
|
+
|
|
156
|
+
if line:
|
|
157
|
+
lines.append(line)
|
|
158
|
+
|
|
159
|
+
console.print("{")
|
|
160
|
+
for line in lines:
|
|
161
|
+
console.print(" ", end="")
|
|
162
|
+
console.print(line)
|
|
163
|
+
console.print("}")
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.progress import (
|
|
5
|
+
Progress,
|
|
6
|
+
SpinnerColumn,
|
|
7
|
+
TextColumn,
|
|
8
|
+
TimeElapsedColumn,
|
|
9
|
+
)
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from typing_extensions import Annotated
|
|
13
|
+
|
|
14
|
+
from exosphere import app_config, context
|
|
15
|
+
from exosphere.objects import Host
|
|
16
|
+
|
|
17
|
+
# Steal the save function from inventory command
|
|
18
|
+
from .inventory import save as save_inventory
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
help="Host management commands",
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
err_console = Console(stderr=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_inventory():
|
|
30
|
+
"""
|
|
31
|
+
Get the inventory from context
|
|
32
|
+
A convenience wrapper that bails if the inventory is not initialized.
|
|
33
|
+
"""
|
|
34
|
+
if context.inventory is None:
|
|
35
|
+
typer.echo(
|
|
36
|
+
"Inventory is not initialized, are you running this module directly?",
|
|
37
|
+
err=True,
|
|
38
|
+
)
|
|
39
|
+
raise typer.Exit(code=1)
|
|
40
|
+
|
|
41
|
+
return context.inventory
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_host(name: str) -> Host | None:
|
|
45
|
+
"""
|
|
46
|
+
Wraps inventory.get_host() to handle displaying errors on console
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
inventory = _get_inventory()
|
|
50
|
+
|
|
51
|
+
host = inventory.get_host(name)
|
|
52
|
+
|
|
53
|
+
if host is None:
|
|
54
|
+
err_console.print(
|
|
55
|
+
Panel.fit(
|
|
56
|
+
f"Host '{name}' not found in inventory.",
|
|
57
|
+
title="Error",
|
|
58
|
+
style="red",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
return host
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def show(
|
|
68
|
+
name: Annotated[str, typer.Argument(help="Host from inventory to show")],
|
|
69
|
+
include_updates: Annotated[
|
|
70
|
+
bool,
|
|
71
|
+
typer.Option(
|
|
72
|
+
"--updates/--no-updates",
|
|
73
|
+
"-u/-n",
|
|
74
|
+
help="Show update details for the host",
|
|
75
|
+
),
|
|
76
|
+
] = True,
|
|
77
|
+
security_only: Annotated[
|
|
78
|
+
bool,
|
|
79
|
+
typer.Option(
|
|
80
|
+
"--security-only",
|
|
81
|
+
"-s",
|
|
82
|
+
help="Show only security updates for the host when displaying updates",
|
|
83
|
+
),
|
|
84
|
+
] = False,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Show details of a specific host.
|
|
88
|
+
|
|
89
|
+
This command retrieves the host by name from the inventory
|
|
90
|
+
and displays its details in a rich format.
|
|
91
|
+
"""
|
|
92
|
+
host = _get_host(name)
|
|
93
|
+
|
|
94
|
+
if host is None:
|
|
95
|
+
raise typer.Exit(code=1)
|
|
96
|
+
|
|
97
|
+
# Color security updates count
|
|
98
|
+
security_count = (
|
|
99
|
+
f"[red]{len(host.security_updates)}[/red]" if host.security_updates else "0"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# prepare host OS details
|
|
103
|
+
host_os_details = (
|
|
104
|
+
f"{host.flavor} {host.os} {host.version}"
|
|
105
|
+
if host.flavor != host.os
|
|
106
|
+
else f"{host.os} {host.version}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if not host.last_refresh:
|
|
110
|
+
last_refresh = "[red]Never[/red]"
|
|
111
|
+
else:
|
|
112
|
+
# Format: "Fri May 21:04:43 EDT 2025"
|
|
113
|
+
last_refresh = host.last_refresh.strftime("%a %b %d %H:%M:%S %Y")
|
|
114
|
+
|
|
115
|
+
# Display host properties in a rich panel
|
|
116
|
+
console.print(
|
|
117
|
+
Panel.fit(
|
|
118
|
+
f"[bold]Host Name:[/bold] {host.name}\n"
|
|
119
|
+
f"[bold]IP Address:[/bold] {host.ip}\n"
|
|
120
|
+
f"[bold]Port:[/bold] {host.port}\n"
|
|
121
|
+
f"[bold]Online Status:[/bold] {'[bold green]Online[/bold green]' if host.online else '[red]Offline[/red]'}\n"
|
|
122
|
+
"\n"
|
|
123
|
+
f"[bold]Last Refreshed:[/bold] {last_refresh}\n"
|
|
124
|
+
f"[bold]Stale:[/bold] {'[yellow]Yes[/yellow]' if host.is_stale else 'No'}\n"
|
|
125
|
+
"\n"
|
|
126
|
+
f"[bold]Operating System:[/bold]\n"
|
|
127
|
+
f" {host_os_details}, using {host.package_manager}\n"
|
|
128
|
+
"\n"
|
|
129
|
+
f"[bold]Updates Available:[/bold] {len(host.updates)} updates, {security_count} security\n",
|
|
130
|
+
title=host.description if host.description else "Host Details",
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not include_updates:
|
|
135
|
+
# Warn for invalid set of arguments
|
|
136
|
+
if security_only:
|
|
137
|
+
err_console.print(
|
|
138
|
+
"[yellow]Warning: --security-only option is only valid with --updates, ignoring.[/yellow]"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
raise typer.Exit(code=0)
|
|
142
|
+
|
|
143
|
+
update_list = host.updates if not security_only else host.security_updates
|
|
144
|
+
|
|
145
|
+
# Display updates in a rich table, if any
|
|
146
|
+
if not update_list:
|
|
147
|
+
console.print("[bold]No updates available for this host.[/bold]")
|
|
148
|
+
raise typer.Exit(code=0)
|
|
149
|
+
|
|
150
|
+
updates_table = Table(
|
|
151
|
+
"Name",
|
|
152
|
+
"Current Version",
|
|
153
|
+
"New Version",
|
|
154
|
+
"Security",
|
|
155
|
+
"Source",
|
|
156
|
+
title="Available Updates",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
for update in update_list:
|
|
160
|
+
updates_table.add_row(
|
|
161
|
+
f"[bold]{update.name}[/bold]",
|
|
162
|
+
update.current_version if update.current_version else "(NEW)",
|
|
163
|
+
update.new_version,
|
|
164
|
+
"Yes" if update.security else "No",
|
|
165
|
+
Text(update.source or "N/A", no_wrap=True),
|
|
166
|
+
style="on bright_black" if update.security else "default",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
console.print(updates_table)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command()
|
|
173
|
+
def discover(
|
|
174
|
+
name: Annotated[str, typer.Argument(help="Host from inventory to discover")],
|
|
175
|
+
) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Gather platform data for host.
|
|
178
|
+
|
|
179
|
+
This command retrieves the host by name from the inventory
|
|
180
|
+
and synchronizes its platform data.
|
|
181
|
+
"""
|
|
182
|
+
host = _get_host(name)
|
|
183
|
+
|
|
184
|
+
if host is None:
|
|
185
|
+
raise typer.Exit(code=1)
|
|
186
|
+
|
|
187
|
+
with Progress(
|
|
188
|
+
SpinnerColumn(),
|
|
189
|
+
TextColumn("[progress.description]{task.description}"),
|
|
190
|
+
TimeElapsedColumn(),
|
|
191
|
+
) as progress:
|
|
192
|
+
progress.add_task(f"Discovering platform for '{host.name}'", total=None)
|
|
193
|
+
try:
|
|
194
|
+
host.discover()
|
|
195
|
+
except Exception as e:
|
|
196
|
+
progress.console.print(
|
|
197
|
+
Panel.fit(
|
|
198
|
+
f"{str(e)}",
|
|
199
|
+
title="[red]Error[/red]",
|
|
200
|
+
style="red",
|
|
201
|
+
title_align="left",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if app_config["options"]["cache_autosave"]:
|
|
206
|
+
save_inventory()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command()
|
|
210
|
+
def refresh(
|
|
211
|
+
name: Annotated[str, typer.Argument(help="Host from inventory to refresh")],
|
|
212
|
+
full: Annotated[
|
|
213
|
+
bool, typer.Option("--sync", "-s", help="Also refresh package catalog")
|
|
214
|
+
] = False,
|
|
215
|
+
discover: Annotated[
|
|
216
|
+
bool, typer.Option("--discover", "-d", help="Also refresh platform information")
|
|
217
|
+
] = False,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""
|
|
220
|
+
Refresh the updates for a specific host.
|
|
221
|
+
|
|
222
|
+
This command retrieves the host by name from the inventory
|
|
223
|
+
and refreshes its updates.
|
|
224
|
+
"""
|
|
225
|
+
host = _get_host(name)
|
|
226
|
+
|
|
227
|
+
if host is None:
|
|
228
|
+
raise typer.Exit(code=1)
|
|
229
|
+
|
|
230
|
+
with Progress(
|
|
231
|
+
SpinnerColumn(),
|
|
232
|
+
TextColumn("[progress.description]{task.description}"),
|
|
233
|
+
TimeElapsedColumn(),
|
|
234
|
+
) as progress:
|
|
235
|
+
if discover:
|
|
236
|
+
task = progress.add_task(
|
|
237
|
+
f"Refreshing platform information for '{host.name}'", total=None
|
|
238
|
+
)
|
|
239
|
+
try:
|
|
240
|
+
host.discover()
|
|
241
|
+
except Exception as e:
|
|
242
|
+
progress.console.print(
|
|
243
|
+
Panel.fit(
|
|
244
|
+
f"{str(e)}",
|
|
245
|
+
title="[red]Error[/red]",
|
|
246
|
+
style="red",
|
|
247
|
+
title_align="left",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
progress.stop_task(task)
|
|
251
|
+
raise typer.Exit(code=1)
|
|
252
|
+
|
|
253
|
+
progress.stop_task(task)
|
|
254
|
+
|
|
255
|
+
if full:
|
|
256
|
+
task = progress.add_task(
|
|
257
|
+
f"Refreshing package catalog for '{host.name}'", total=None
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
host.refresh_catalog()
|
|
261
|
+
except Exception as e:
|
|
262
|
+
progress.console.print(
|
|
263
|
+
Panel.fit(
|
|
264
|
+
f"{str(e)}",
|
|
265
|
+
title="[red]Error[/red]",
|
|
266
|
+
style="red",
|
|
267
|
+
title_align="left",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
progress.stop_task(task)
|
|
271
|
+
raise typer.Exit(code=1)
|
|
272
|
+
|
|
273
|
+
progress.stop_task(task)
|
|
274
|
+
|
|
275
|
+
task = progress.add_task(f"Refreshing updates for '{host.name}'", total=None)
|
|
276
|
+
try:
|
|
277
|
+
host.refresh_updates()
|
|
278
|
+
except Exception as e:
|
|
279
|
+
progress.console.print(
|
|
280
|
+
Panel.fit(
|
|
281
|
+
f"{str(e)}",
|
|
282
|
+
title="[red]Error[/red]",
|
|
283
|
+
style="red",
|
|
284
|
+
title_align="left",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if app_config["options"]["cache_autosave"]:
|
|
289
|
+
save_inventory()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@app.command()
|
|
293
|
+
def ping(
|
|
294
|
+
name: Annotated[str, typer.Argument(help="Host from inventory to ping")],
|
|
295
|
+
) -> None:
|
|
296
|
+
"""
|
|
297
|
+
Ping a specific host to check its reachability.
|
|
298
|
+
|
|
299
|
+
This command will also update a host's online status
|
|
300
|
+
based on the ping result.
|
|
301
|
+
|
|
302
|
+
The ping is is based on ssh connectivity.
|
|
303
|
+
"""
|
|
304
|
+
host = _get_host(name)
|
|
305
|
+
|
|
306
|
+
if host is None:
|
|
307
|
+
raise typer.Exit(code=1)
|
|
308
|
+
|
|
309
|
+
if host.ping():
|
|
310
|
+
console.print(
|
|
311
|
+
f"Host [bold]{host.name}[/bold] is [bold green]Online[/bold green]."
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
console.print(f"Host [bold]{host.name}[/bold] is [red]Offline[/red].")
|
|
315
|
+
|
|
316
|
+
if app_config["options"]["cache_autosave"]:
|
|
317
|
+
save_inventory()
|