exosphere-cli 2.4.1__tar.gz → 2.4.2__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.
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/PKG-INFO +1 -1
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/pyproject.toml +4 -4
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/providers/debian.py +21 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/providers/redhat.py +8 -2
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/setup/detect.py +26 -29
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/LICENSE +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/README.md +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/__init__.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/cli.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/__init__.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/config.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/connections.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/host.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/inventory.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/report.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/sudo.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/ui.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/utils.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/commands/version.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/config.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/context.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/data.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/database.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/errors.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/fspaths.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/inventory.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/main.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/migrations.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/objects.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/pipelining.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/providers/__init__.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/providers/api.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/providers/factory.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/providers/freebsd.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/providers/openbsd.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/repl.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/reporting.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/schema/__init__.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/schema/host-report.schema.json +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/security.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/setup/__init__.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/templates/report.html.j2 +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/templates/report.md.j2 +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/templates/report.txt.j2 +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/__init__.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/app.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/context.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/dashboard.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/elements.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/inventory.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/logs.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/messages.py +0 -0
- {exosphere_cli-2.4.1 → exosphere_cli-2.4.2}/src/exosphere/ui/style.tcss +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "exosphere-cli"
|
|
3
|
-
version = "2.4.
|
|
3
|
+
version = "2.4.2"
|
|
4
4
|
description = "CLI/TUI driven patch reporting for remote Unix-like systems."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -48,14 +48,14 @@ dev = [
|
|
|
48
48
|
"pytest-cov>=6.1.1",
|
|
49
49
|
"pytest-json-ctrf>=0.3.5",
|
|
50
50
|
"pytest-mock>=3.14.1",
|
|
51
|
-
"
|
|
51
|
+
"sphinx-rtd-theme>=3.0.2",
|
|
52
52
|
"ruff>=0.15.0",
|
|
53
|
-
"sphinx>=8.2.3
|
|
53
|
+
"sphinx>=8.2.3",
|
|
54
54
|
"sphinx-autobuild>=2024.10.3",
|
|
55
55
|
"sphinx-lint>=1.0.0",
|
|
56
56
|
"sphinx-tabs>=3.4.7",
|
|
57
57
|
"sphinxcontrib-spelling>=8.0.1",
|
|
58
|
-
"sphinxcontrib-typer>=0.
|
|
58
|
+
"sphinxcontrib-typer>=0.7.2",
|
|
59
59
|
"textual-dev>=1.7.0",
|
|
60
60
|
]
|
|
61
61
|
|
|
@@ -50,6 +50,10 @@ class Apt(PkgManager):
|
|
|
50
50
|
)
|
|
51
51
|
return False
|
|
52
52
|
|
|
53
|
+
# Log warnings from apt-get if there was any stderr output.
|
|
54
|
+
if update.stderr:
|
|
55
|
+
self._log_apt_warn(update.stderr)
|
|
56
|
+
|
|
53
57
|
self.logger.debug("Apt repositories synchronized successfully")
|
|
54
58
|
|
|
55
59
|
return True
|
|
@@ -79,6 +83,10 @@ class Apt(PkgManager):
|
|
|
79
83
|
self.logger.debug("No updates available or no matches in output.")
|
|
80
84
|
return updates
|
|
81
85
|
|
|
86
|
+
# Log warnings from apt-get if there was any stderr output.
|
|
87
|
+
if raw_query.stderr:
|
|
88
|
+
self._log_apt_warn(raw_query.stderr)
|
|
89
|
+
|
|
82
90
|
for line in raw_query.stdout.splitlines():
|
|
83
91
|
line = line.strip()
|
|
84
92
|
|
|
@@ -151,3 +159,16 @@ class Apt(PkgManager):
|
|
|
151
159
|
source=repo_source,
|
|
152
160
|
security=is_security,
|
|
153
161
|
)
|
|
162
|
+
|
|
163
|
+
def _log_apt_warn(self, lines: str) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Log warnings from APT stderr output.
|
|
166
|
+
We ignore empty lines and lines that do not begin with "W:"
|
|
167
|
+
|
|
168
|
+
:param lines: Stderr output to log.
|
|
169
|
+
"""
|
|
170
|
+
for line in lines.splitlines():
|
|
171
|
+
line = line.strip()
|
|
172
|
+
if not line or not line.startswith("W: "):
|
|
173
|
+
continue
|
|
174
|
+
self.logger.warning("APT: %s", line)
|
|
@@ -216,11 +216,14 @@ class Dnf(PkgManager):
|
|
|
216
216
|
"""
|
|
217
217
|
|
|
218
218
|
# Lazy compile the line pattern on first use
|
|
219
|
+
# Repository source component is usually in the form of "reponame"
|
|
220
|
+
# but can be prefixed with "@" or in the case of missing metadata,
|
|
221
|
+
# be the string "<unknown>" or similar. We account for these here.
|
|
219
222
|
if self.line_pattern is None:
|
|
220
223
|
self.line_pattern = re.compile(
|
|
221
224
|
r"^(?P<name>[a-z0-9][\w+.-]*\.\w+)\s+" # Package (name.arch)
|
|
222
225
|
r"(?P<version>[\w.+~:-]+-[\w.+~]+)\s+" # RPM version-release
|
|
223
|
-
r"(?P<source>@?[a-z0-9][\w.:+/-]
|
|
226
|
+
r"(?P<source>@?[a-z0-9][\w.:+/-]*|<[^>\s]+>)$", # Repo source (optional @ or <>
|
|
224
227
|
re.ASCII | re.IGNORECASE,
|
|
225
228
|
)
|
|
226
229
|
|
|
@@ -230,7 +233,10 @@ class Dnf(PkgManager):
|
|
|
230
233
|
self.logger.debug("Skipping garbage line: %s", line)
|
|
231
234
|
return None
|
|
232
235
|
|
|
233
|
-
|
|
236
|
+
# Cleanup source string before sending it up
|
|
237
|
+
source = match["source"].removeprefix("@").strip("<>")
|
|
238
|
+
|
|
239
|
+
return (match["name"], match["version"], source)
|
|
234
240
|
|
|
235
241
|
def _get_current_version(
|
|
236
242
|
self, cx: Connection, package_names: list[str]
|
|
@@ -30,6 +30,10 @@ def platform_detect(cx: Connection) -> HostInfo:
|
|
|
30
30
|
Detect the platform of the remote system.
|
|
31
31
|
Entry point for refreshing all platform details.
|
|
32
32
|
|
|
33
|
+
Linux detection in general depends on the presence of a well-formed
|
|
34
|
+
/etc/os-release file, which is a safe assumption for everything we
|
|
35
|
+
explicitly support that's not EOL'd.
|
|
36
|
+
|
|
33
37
|
:param cx: Fabric Connection object
|
|
34
38
|
:return: HostInfo object with platform details
|
|
35
39
|
"""
|
|
@@ -114,8 +118,6 @@ def flavor_detect(cx: Connection, platform_name: str) -> str:
|
|
|
114
118
|
|
|
115
119
|
# Linux
|
|
116
120
|
if platform_name == "linux":
|
|
117
|
-
# We're just going to query /etc/os-release directly.
|
|
118
|
-
# Using lsb_release would be better, but it's less available
|
|
119
121
|
result_id = cx.run("grep ^ID= /etc/os-release", hide=True, warn=True)
|
|
120
122
|
result_like_id = cx.run(
|
|
121
123
|
"grep ^ID_LIKE= /etc/os-release",
|
|
@@ -125,7 +127,7 @@ def flavor_detect(cx: Connection, platform_name: str) -> str:
|
|
|
125
127
|
|
|
126
128
|
if result_id.failed:
|
|
127
129
|
raise DataRefreshError(
|
|
128
|
-
"Failed to detect OS flavor via
|
|
130
|
+
"Failed to detect OS flavor via os-release identifier.",
|
|
129
131
|
stderr=result_id.stderr,
|
|
130
132
|
stdout=result_id.stdout,
|
|
131
133
|
)
|
|
@@ -187,36 +189,31 @@ def version_detect(cx: Connection, flavor_name: str) -> str:
|
|
|
187
189
|
if flavor_name.lower() not in SUPPORTED_FLAVORS:
|
|
188
190
|
raise UnsupportedOSError(f"Unsupported OS flavor: {flavor_name}")
|
|
189
191
|
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
# Linux flavors that rely on os-release metadata
|
|
193
|
+
# (which is all of them, at the time of writing)
|
|
194
|
+
if flavor_name in ["ubuntu", "debian", "rhel", "fedora"]:
|
|
195
|
+
result_version = None
|
|
196
|
+
|
|
197
|
+
# Some systems (debian sid, for instance) don't provide VERSION_ID,
|
|
198
|
+
# So we fall back to VERSION_CODENAME in these cases.
|
|
199
|
+
# If neither work, we just err on the side of failure.
|
|
200
|
+
for version_key in ["VERSION_ID", "VERSION_CODENAME"]:
|
|
201
|
+
result_version = cx.run(
|
|
202
|
+
f"grep ^{version_key}= /etc/os-release", hide=True, warn=True
|
|
199
203
|
)
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
result_version = cx.run(
|
|
206
|
-
"grep ^VERSION_ID= /etc/os-release", hide=True, warn=True
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if result_version.failed:
|
|
210
|
-
raise DataRefreshError(
|
|
211
|
-
"Failed to detect OS version via os-release VERSION_ID.",
|
|
212
|
-
stderr=result_version.stderr,
|
|
213
|
-
stdout=result_version.stdout,
|
|
214
|
-
)
|
|
205
|
+
if not result_version.failed:
|
|
206
|
+
logger.debug("Found version using %s", version_key)
|
|
207
|
+
version_line = result_version.stdout.strip()
|
|
208
|
+
version_value = version_line.partition("=")[2].strip().strip("\"'")
|
|
215
209
|
|
|
216
|
-
|
|
217
|
-
version_value = version_line.partition("=")[2].strip().strip("\"'")
|
|
210
|
+
return version_value
|
|
218
211
|
|
|
219
|
-
|
|
212
|
+
raise DataRefreshError(
|
|
213
|
+
"Failed to detect OS version via os-release VERSION_ID or VERSION_CODENAME.",
|
|
214
|
+
stderr=result_version.stderr if result_version else "",
|
|
215
|
+
stdout=result_version.stdout if result_version else "",
|
|
216
|
+
)
|
|
220
217
|
|
|
221
218
|
# FreeBSD
|
|
222
219
|
if flavor_name == "freebsd":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|