|
20 | 20 |
|
21 | 21 | from automax import __version__ |
22 | 22 | from automax.core.engine import AutomaxEngine, AutomaxError |
| 23 | +from automax.core.capabilities import package_for_tool |
23 | 24 | from automax.core.inventory import Inventory, InventoryError, load_inventory_document |
24 | 25 | from automax.core.known_hosts import KnownHostsError, scan_known_hosts, write_known_hosts |
25 | 26 | from automax.core.plugin_docs import render_plugin_reference |
@@ -121,55 +122,125 @@ def _echo_vars_payload(payload: Dict[str, Any], output_format: str) -> None: |
121 | 122 | click.echo(f" {node['node_id']} {node['plugin']}") |
122 | 123 |
|
123 | 124 |
|
| 125 | +def _package_for_display(tool: str, target: Dict[str, Any]) -> str | None: |
| 126 | + family = target.get("os", {}).get("family", "unknown") |
| 127 | + return package_for_tool(tool, family) |
| 128 | + |
| 129 | + |
124 | 130 | def _echo_capabilities_payload(payload: Dict[str, Any], output_format: str) -> None: |
125 | 131 | if output_format == "json": |
126 | 132 | click.echo(json.dumps(payload, indent=2, sort_keys=True)) |
127 | 133 | return |
128 | 134 | click.echo(f"Job: {payload['job']}") |
129 | | - click.echo(f"Targets: {payload['target_count']} Tools: {payload['tool_count']} Packages: {payload.get('package_count', 0)}") |
| 135 | + click.echo( |
| 136 | + f"Targets: {payload['target_count']} " |
| 137 | + f"Tools: {payload['tool_count']} " |
| 138 | + f"Packages: {payload.get('package_count', 0)} " |
| 139 | + f"Missing tools: {payload.get('missing_tool_count', 0)} " |
| 140 | + f"Missing packages: {payload.get('missing_package_count', 0)}" |
| 141 | + ) |
130 | 142 | for target in payload["targets"]: |
131 | 143 | click.echo(f"Target {target['target']} {target['host']} os={target.get('os', {}).get('family', 'unknown')} id={target.get('os', {}).get('id', 'unknown')}") |
132 | 144 | if target.get("skipped_plugins"): |
133 | 145 | click.echo(" skipped OS-mismatched plugins:") |
134 | 146 | for skipped in target["skipped_plugins"]: |
135 | 147 | click.echo(f" {skipped['plugin']}: {skipped['reason']}") |
136 | 148 | if target.get("packages"): |
137 | | - click.echo(" packages:") |
| 149 | + click.echo(" required packages:") |
138 | 150 | for package in target["packages"]: |
139 | 151 | click.echo(f" {package}") |
| 152 | + if "missing_tools" in target: |
| 153 | + if target.get("missing_tools"): |
| 154 | + click.echo(" missing tools:") |
| 155 | + for tool in target["missing_tools"]: |
| 156 | + package = _package_for_display(tool, target) |
| 157 | + suffix = f" -> package {package}" if package else " -> package unknown" |
| 158 | + click.echo(f" {tool}{suffix}") |
| 159 | + else: |
| 160 | + click.echo(" missing tools: none") |
| 161 | + if target.get("missing_packages"): |
| 162 | + click.echo(" missing packages:") |
| 163 | + for package in target["missing_packages"]: |
| 164 | + click.echo(f" {package}") |
| 165 | + else: |
| 166 | + click.echo(" missing packages: none") |
| 167 | + if target.get("unresolved_tools"): |
| 168 | + click.echo(" unresolved tools:") |
| 169 | + for tool in target["unresolved_tools"]: |
| 170 | + click.echo(f" {tool}") |
140 | 171 | if not target["tools"]: |
141 | 172 | click.echo(" - no external tool requirements detected") |
142 | 173 | continue |
143 | 174 | for tool in target["tools"]: |
144 | 175 | plugins = ", ".join(target["plugins"].get(tool, [])) |
145 | | - click.echo(f" {tool}: {plugins}") |
| 176 | + state = "" |
| 177 | + if tool in set(target.get("missing_tools", [])): |
| 178 | + state = " [missing]" |
| 179 | + elif "present_tools" in target: |
| 180 | + state = " [present]" |
| 181 | + click.echo(f" {tool}{state}: {plugins}") |
146 | 182 | click.echo(" preflight commands:") |
147 | 183 | for command in target["commands"]: |
148 | 184 | click.echo(f" {command}") |
149 | 185 |
|
150 | 186 |
|
| 187 | +def _format_csv(values: Iterable[str]) -> str: |
| 188 | + values = list(values) |
| 189 | + return ", ".join(values) if values else "none" |
| 190 | + |
| 191 | + |
| 192 | +def _echo_capability_install_event(event: Dict[str, Any]) -> None: |
| 193 | + kind = event.get("event") |
| 194 | + if kind == "job": |
| 195 | + click.echo(f"Job: {event['job']}") |
| 196 | + return |
| 197 | + target = event.get("target", "-") |
| 198 | + host = event.get("host", "-") |
| 199 | + os_family = event.get("os", {}).get("family", "unknown") |
| 200 | + if kind == "target-check": |
| 201 | + click.echo(f"[CHECK] {target} {host} os={os_family}: checking required tools") |
| 202 | + return |
| 203 | + if kind == "target-missing": |
| 204 | + missing_tools = event.get("missing_tools", []) |
| 205 | + packages = event.get("packages", []) |
| 206 | + unresolved = event.get("unresolved_tools", []) |
| 207 | + if missing_tools: |
| 208 | + click.echo(f"[MISSING] {target}: tools={_format_csv(missing_tools)}") |
| 209 | + else: |
| 210 | + click.echo(f"[OK] {target}: all required tools already present") |
| 211 | + if packages: |
| 212 | + click.echo(f"[PLAN] {target}: install packages={_format_csv(packages)}") |
| 213 | + if unresolved: |
| 214 | + click.echo(f"[WARN] {target}: unresolved tools={_format_csv(unresolved)}") |
| 215 | + return |
| 216 | + if kind == "target-install": |
| 217 | + click.echo(f"[INSTALL] {target}: packages={_format_csv(event.get('packages', []))}") |
| 218 | + return |
| 219 | + if kind == "target-done": |
| 220 | + status = "OK" if event.get("ok") else "FAILED" |
| 221 | + changed = "changed=true" if event.get("changed") else "changed=false" |
| 222 | + click.echo(f"[{status}] {target} {host} os={os_family} rc={event.get('rc', 0)} {changed}") |
| 223 | + |
| 224 | + |
151 | 225 | def _echo_capability_install_payload(payload: Dict[str, Any], output_format: str) -> None: |
152 | 226 | if output_format == "json": |
153 | 227 | click.echo(json.dumps(payload, indent=2, sort_keys=True)) |
154 | 228 | return |
155 | | - click.echo(f"Job: {payload['job']}") |
| 229 | + ok_targets = sum(1 for target in payload["targets"] if target["ok"]) |
| 230 | + failed_targets = len(payload["targets"]) - ok_targets |
| 231 | + changed_targets = sum(1 for target in payload["targets"] if target["changed"]) |
| 232 | + click.echo("Summary:") |
| 233 | + click.echo(f" targets: {len(payload['targets'])}") |
| 234 | + click.echo(f" ok: {ok_targets}") |
| 235 | + click.echo(f" failed: {failed_targets}") |
| 236 | + click.echo(f" changed: {changed_targets}") |
156 | 237 | for target in payload["targets"]: |
157 | | - status = "OK" if target["ok"] else "FAILED" |
158 | | - click.echo(f"[{status}] {target['target']} {target['host']} os={target['os']['family']} rc={target['rc']}") |
159 | | - if target["missing_tools"]: |
160 | | - click.echo(" missing tools:") |
161 | | - for tool in target["missing_tools"]: |
162 | | - click.echo(f" {tool}") |
163 | | - if target["packages"]: |
164 | | - click.echo(" installed packages:") |
165 | | - for package in target["packages"]: |
166 | | - click.echo(f" {package}") |
167 | | - if target["unresolved_tools"]: |
168 | | - click.echo(" unresolved tools:") |
169 | | - for tool in target["unresolved_tools"]: |
170 | | - click.echo(f" {tool}") |
| 238 | + if target["stdout"]: |
| 239 | + click.echo(f" {target['target']} stdout:") |
| 240 | + for line in target["stdout"].splitlines(): |
| 241 | + click.echo(f" {line}") |
171 | 242 | if target["stderr"]: |
172 | | - click.echo(" stderr:") |
| 243 | + click.echo(f" {target['target']} stderr:") |
173 | 244 | for line in target["stderr"].splitlines(): |
174 | 245 | click.echo(f" {line}") |
175 | 246 |
|
@@ -1098,6 +1169,7 @@ def capability_requirements( |
1098 | 1169 | tags=_split_selectors(tags), |
1099 | 1170 | skip_tags=_split_selectors(skip_tags), |
1100 | 1171 | cli_vars=_parse_vars(cli_vars), |
| 1172 | + check_missing=output_format == "text", |
1101 | 1173 | ) |
1102 | 1174 | except (AutomaxError, ValueError, RuntimeError) as exc: |
1103 | 1175 | raise click.ClickException(str(exc)) from exc |
@@ -1147,6 +1219,7 @@ def capability_install( |
1147 | 1219 | skip_tags=_split_selectors(skip_tags), |
1148 | 1220 | cli_vars=_parse_vars(cli_vars), |
1149 | 1221 | sudo_password_env=sudo_password_env, |
| 1222 | + progress_callback=_echo_capability_install_event if output_format == "text" else None, |
1150 | 1223 | ) |
1151 | 1224 | except (AutomaxError, ValueError, RuntimeError) as exc: |
1152 | 1225 | raise click.ClickException(str(exc)) from exc |
|
0 commit comments