-
Notifications
You must be signed in to change notification settings - Fork 10
ENT-14320: Wrapped most of the remaining cf-remote/cfbs commands into CFEngine-cli #209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
beb5b49
76d1312
7e40bed
8683f5f
68833f5
48674ee
e8fa6bf
a26b902
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,10 +7,93 @@ | |
| add_uninstall_args, | ||
| add_spawn_args, | ||
| add_destroy_args, | ||
| add_connect_args, | ||
| ) | ||
|
|
||
|
|
||
| def parse_wrapper_args(subp: argparse._SubParsersAction): | ||
| update_parser = subp.add_parser( | ||
| "update", | ||
| help="Updates the current cfbs project", | ||
| description="A wrapper around the cfbs `update` function", | ||
| ) | ||
| update_parser.add_argument( | ||
| "to_update", | ||
| nargs="*", | ||
| help="Directory of cfbs-project to update", | ||
| ) | ||
| remove_parser = subp.add_parser( | ||
| "remove", | ||
| help="Removes the specified module(s) from cfbs project", | ||
| description="A wrapper around the cfbs `remove` function", | ||
| ) | ||
| remove_parser.add_argument( | ||
| "module", | ||
| nargs="+", | ||
| help="Module(s) for which to remove", | ||
| ) | ||
|
|
||
| add_parser = subp.add_parser( | ||
| "add", | ||
| help="Adds the specified module(s) to cfbs project", | ||
| description="A wrapper around the cfbs `add` function", | ||
| ) | ||
| add_parser.add_argument( | ||
| "module", | ||
| nargs="+", | ||
| help="Module(s) for which to add", | ||
| ) | ||
| search_parser = subp.add_parser( | ||
| "search", | ||
| help="Searches the build-index for specified module(s)", | ||
| description="A wrapper around the cfbs `search` function", | ||
| ) | ||
| search_parser.add_argument( | ||
| "module", | ||
| nargs="+", | ||
| help="Module(s) for which to lookup", | ||
| ) | ||
|
|
||
| input_parser = subp.add_parser( | ||
| "input", | ||
| help="Sets/updates input.json for selected module(s)", | ||
| description="A wrapper around the cfbs `input` function", | ||
| ) | ||
| input_parser.add_argument( | ||
| "module", | ||
| nargs="+", | ||
| help="Module(s) for which to set input", | ||
| ) | ||
|
|
||
| add_connect_args( | ||
| subp.add_parser( | ||
| "connect", | ||
| help="Opens interactive ssh shell", | ||
| description="A wrapper around cf-remote `connect` function", | ||
| ) | ||
| ) | ||
|
|
||
| moduleinfo_parser = subp.add_parser( | ||
| "moduleinfo", | ||
| help="Shows information about your cfbs-project or a specific module", | ||
| description="A wrapper around the cfbs `status` function", | ||
| ) | ||
|
|
||
| moduleinfo_parser.add_argument( | ||
| "modules", | ||
| nargs="*", | ||
| help="Module(s) for which you would like more info, utilizes cfbs `info` function", | ||
| ) | ||
|
|
||
| show_parser = subp.add_parser( | ||
| "show", help="Shows your saved host-groups or info about a specified host" | ||
| ) | ||
| show_parser.add_argument( | ||
| "--hosts", | ||
| "--host", | ||
| "-H", | ||
| help="Shows more specific information about specific host(s)", | ||
| ) | ||
|
|
||
| add_save_args( | ||
| subp.add_parser( | ||
|
|
@@ -29,18 +112,29 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): | |
| default=None, | ||
| ) | ||
|
|
||
| subp.add_parser( | ||
| sp = subp.add_parser( | ||
| "build", | ||
| help="""Build a policy set from a CFEngine Build project. | ||
| A wrapper around the cfbs `build`-function.""", | ||
| help="Build a policy set from a CFEngine Build project", | ||
| description="A wrapper around the cf-remote `build`-function with some added niceties", | ||
| ) | ||
| sp.add_argument( | ||
| "--non-interactive", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't the whole point with CFEngine CLI that it should not be used in scripts? I feel options like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for most commands, only init and for build/deploy. I added them as niceties when testing a module, that way I could just do |
||
| help="Non-interactive mode (picks the default for all prompts)", | ||
| action="store_true", | ||
| ) | ||
| sp.add_argument("--hub", help="Hub(s) to deploy to after building", type=str) | ||
|
|
||
| deploy_parser = subp.add_parser( | ||
| "deploy", | ||
| help="""Deploy policy-set (masterfiles) to hub. | ||
| A wrapper around the cf-remote `deploy`-function with some added niceties.""", | ||
| help="Deploy policy-set (masterfiles) to hub.", | ||
| description="A wrapper around the cf-remote `deploy`-function with some added niceties.", | ||
| ) | ||
| add_deploy_args(deploy_parser) | ||
| deploy_parser.add_argument( | ||
| "--non-interactive", | ||
| help="Non-interactive mode (picks the default for all prompts)", | ||
| action="store_true", | ||
| ) | ||
|
|
||
| install_parser = subp.add_parser( | ||
| "install", | ||
|
|
@@ -81,8 +175,8 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): | |
|
|
||
| run_parser = subp.add_parser( | ||
| "run", | ||
| description="Run the CFEngine agent, fetching, evaluating, and enforcing policy.\n\ | ||
| A wrapper around the cf-remote `run`-function with some added niceties", | ||
| help="Run the CFEngine agent, fetching, evaluating, and enforcing policy.", | ||
| description="A wrapper around the cf-remote `run`-function with some added niceties", | ||
| epilog="""Examples: | ||
| `cfengine run` defaults to use `cf-agent -KIf update.cf && cf-agent -KI` | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,20 @@ | ||
| import os | ||
|
|
||
| from cfbs.commands import build_command | ||
| from cfbs.commands import build_command, info_command, status_command | ||
| from cf_remote import log | ||
| from cf_remote.commands import deploy as deploy_command | ||
| from cf_remote.commands import deploy as deploy_command, info | ||
| from cf_remote.commands import destroy as destroy_command | ||
| from cf_remote.commands import save as save_command | ||
| from cf_remote.commands import show as show_command | ||
| from cf_remote.remote import run_command, transfer_file | ||
| from cf_remote.commands import connect_cmd | ||
| from cfbs.commands import ( | ||
| input_command, | ||
| add_command, | ||
| remove_command, | ||
| update_command, | ||
| search_command, | ||
| ) | ||
|
|
||
| from cfengine_cli.utils import UserError | ||
| from cfengine_cli.cfengine_wrapper.cfengine_objects import ( | ||
|
|
@@ -228,17 +237,87 @@ def destroy(groupname, del_all=False) -> int: | |
| return destroy_command(groupname) | ||
|
|
||
|
|
||
| def build() -> int: | ||
| def build(hub=None, non_interactive=False) -> int: | ||
| rc = build_command() | ||
| if rc != 0: | ||
| return rc | ||
| if prompt_yes_no("Deploy the built policy set now?", default=True): | ||
| return deploy(None, None) | ||
| if prompt_yes_no( | ||
| "Deploy the built policy set now?", | ||
| default=True, | ||
| non_interactive=non_interactive, | ||
| ): | ||
| return deploy(hub, None, non_interactive) | ||
| return 0 | ||
|
|
||
|
|
||
| def deploy(target: str | list[str] | None, masterfiles: str | None = None) -> int: | ||
| def deploy( | ||
| target: str | list[str] | None, | ||
| masterfiles: str | None = None, | ||
| non_interactive: bool = False, | ||
| ) -> int: | ||
| error = 0 | ||
| if isinstance(target, str): | ||
| target = [target] | ||
| hubs = [require_executable("cf-agent", h).location for h in (target or [])] or None | ||
| return deploy_command(hubs, masterfiles) | ||
| hubs = { | ||
| x.location: x | ||
| for h in (target or []) | ||
| for x in [require_executable("cf-agent", h)] | ||
| } or None | ||
|
|
||
| # TODO/WOULD be nice: Deploy without run | ||
|
SimonThalvorsen marked this conversation as resolved.
|
||
| if hubs: | ||
| error = deploy_command(hubs.keys(), masterfiles) | ||
| else: | ||
| return deploy_command(hubs, masterfiles) | ||
|
|
||
| if prompt_yes_no( | ||
| "Run policy set now?", default=True, non_interactive=non_interactive | ||
| ): | ||
| for hub in hubs: | ||
| hubs[hub].run("-KIf update.cf", "-KI") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ref. comment above. Will this run the agent again?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, thats why a deploy without run would be nice. The automatic run after deployment catches the output unless you specify log-level DEBUG |
||
| return error | ||
|
|
||
|
|
||
| def show(target: list[str] | None = None) -> int: | ||
| if target == [] or target is None: | ||
| return show_command(False) | ||
| if isinstance(target, str): | ||
| target = [target] | ||
| return info(target) | ||
|
|
||
|
|
||
| def moduleinfo(modules: list[str]) -> int: | ||
| if modules == []: | ||
| try: | ||
| status_command() | ||
| except Exception as e: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't you just check of you are inside a cfbs-project, rather than catching all exceptions? This would hide bugs like out of bound array access, etc. |
||
| log.error( | ||
| f"Failed to validate cfbs-status, make sure you are inside a cfbs-project: {e}" | ||
| ) | ||
| return -1 | ||
|
|
||
| return info_command(modules) | ||
|
|
||
|
|
||
| def connect(host) -> int: | ||
| return connect_cmd(host) | ||
|
|
||
|
|
||
| def cfbs_input(modules: list[str]) -> int: | ||
| return input_command(modules, "cfengine input") | ||
|
|
||
|
|
||
| def cfbs_add(modules: list[str]) -> int: | ||
| return add_command(modules, "cfengine input") | ||
|
|
||
|
|
||
| def cfbs_remove(modules: list[str] | None = None) -> int: | ||
| return remove_command(modules, "cfengine input") | ||
|
|
||
|
|
||
| def cfbs_update(to_update) -> int: | ||
| return update_command(to_update) | ||
|
|
||
|
|
||
| def cfbs_search(modules: list[str]) -> int: | ||
| return search_command(modules) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| from collections import namedtuple | ||
| import os | ||
| import shutil | ||
| import random | ||
|
|
@@ -16,7 +17,9 @@ | |
| DEFAULT_MAX_REPORT_HOSTS = 25 | ||
|
|
||
|
|
||
| def prompt_yes_no(prompt: str, default: bool = True) -> bool: | ||
| def prompt_yes_no(prompt: str, default: bool = True, non_interactive=False) -> bool: | ||
| if non_interactive: | ||
| return default | ||
| suffix = "[Y/n]" if default else "[y/N]" | ||
| answer = input(f"{prompt} {suffix} ").strip().lower() | ||
| if not answer: | ||
|
|
@@ -103,21 +106,34 @@ def _hosts_with_info(role_filter=None): | |
| yield host, aliases, data | ||
|
|
||
|
|
||
| def _find_all(binary_name: str) -> list[Executable]: | ||
| """Every location -- local, plus every matching remote host -- with `binary_name` installed.""" | ||
| executables = [] | ||
| _Id = namedtuple("_Id", "location aliases") | ||
|
|
||
|
|
||
| def _identities(binary_name: str) -> Iterator[_Id]: | ||
| """local + every known host as (location, aliases), without connecting.""" | ||
| yield _Id("local", []) | ||
| for host, aliases in _known_hosts(None if binary_name == "cf-agent" else "hub"): | ||
| yield _Id(host, aliases) | ||
|
|
||
|
|
||
| def _resolve(binary_name: str, ident: _Id) -> Executable | None: | ||
| """Connect (if remote) and build an Executable, or None if unavailable.""" | ||
| if ident.location == "local": | ||
| path = _find_local_path(binary_name) | ||
| return Executable(binary_name, "local", path) if path else None | ||
| data = _host_info(ident.location) | ||
| if not data: | ||
| return None | ||
| # band-aid: hostinfo has no path for cf-hub, so assume it's on PATH | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you can create a follow-up ticket for this? |
||
| path = data.get("agent") if binary_name == "cf-agent" else "cf-hub" | ||
| return ( | ||
| Executable(binary_name, ident.location, path, ident.aliases) if path else None | ||
| ) | ||
|
|
||
| local_path = _find_local_path(binary_name) | ||
| if local_path: | ||
| executables.append(Executable(binary_name, "local", local_path)) | ||
|
|
||
| is_agent = binary_name == "cf-agent" | ||
| for host, aliases, data in _hosts_with_info(None if is_agent else "hub"): | ||
| # band-aid: hostinfo has no path for cf-hub, so assume it's on PATH | ||
| path = data.get("agent") if is_agent else "cf-hub" | ||
| if path: | ||
| executables.append(Executable(binary_name, host, path, aliases)) | ||
| return executables | ||
| def _find_all(binary_name: str) -> list[Executable]: | ||
| """Every location with `binary_name` installed (connects to all).""" | ||
| return [e for i in _identities(binary_name) if (e := _resolve(binary_name, i))] | ||
|
|
||
|
|
||
| def _find_all_paired() -> list[Installation]: | ||
|
|
@@ -195,7 +211,35 @@ def _select(candidates, description, target: str | None = None): | |
|
|
||
|
|
||
| def require_executable(name: str, target: str | None = None) -> Executable: | ||
| chosen = _select(_find_all(name), name, target) | ||
| if isinstance(target, list): | ||
| if len(target) > 1: | ||
| raise UserError( | ||
| f"Expected a single {name}, but got {len(target)}: {', '.join(target)}." | ||
| ) | ||
| target = target[0] if target else None | ||
|
|
||
| if target: | ||
| idents = list(_identities(name)) | ||
| matched = [i for i in idents if _exact_match(i, target)] or [ | ||
| i for i in idents if _loose_match(i, target) | ||
| ] | ||
| if not matched: | ||
| raise UserError( | ||
| f"No installation of {name} matches '{target}'. " | ||
| f"Known: {', '.join(i.location for i in idents)}." | ||
| ) | ||
| candidates = [e for i in matched if (e := _resolve(name, i))] | ||
| if not candidates: | ||
| raise UserError( | ||
| f"'{target}' matches a known host for {name}, but it is " | ||
| f"unreachable or lacks {name}." | ||
| ) | ||
| chosen = ( | ||
| candidates[0] if len(candidates) == 1 else _prompt_choice(candidates, name) | ||
| ) | ||
| else: | ||
| chosen = _select(_find_all(name), name) | ||
|
|
||
| log.info( | ||
| f"Using {'local' if chosen.is_local else 'remote'} installation of {name} ({chosen.label})" | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -222,9 +222,21 @@ def run_command_with_args(args) -> int: | |
| if args.command == "init": | ||
| return commands.init(args) | ||
| if args.command == "build": | ||
| return cfengine_commands.build() | ||
| return cfengine_commands.build(args.hub, args.non_interactive) | ||
| if args.command == "deploy": | ||
| return cfengine_commands.deploy(args.hub, args.masterfiles) | ||
| return cfengine_commands.deploy( | ||
| args.hub, args.masterfiles, args.non_interactive | ||
| ) | ||
| if args.command == "input": | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At some point, these should be a dictionary look-ups with functions. I think it's also possible to add functions directly into the argument parser. However, this is probably out of scope for this PR. |
||
| return cfengine_commands.cfbs_input(args.module) | ||
| if args.command == "add": | ||
| return cfengine_commands.cfbs_add(args.module) | ||
| if args.command == "remove": | ||
| return cfengine_commands.cfbs_remove(args.module) | ||
| if args.command == "search": | ||
| return cfengine_commands.cfbs_search(args.module) | ||
| if args.command == "update": | ||
| return cfengine_commands.cfbs_update(args.to_update) | ||
| if args.command == "format": | ||
| return commands.format(args.files, args.line_length, args.check) | ||
| if args.command == "lint": | ||
|
|
@@ -325,6 +337,12 @@ def run_command_with_args(args) -> int: | |
| return commands.profile(args) | ||
| if args.command == "up": | ||
| return commands.up(args) | ||
| if args.command == "show": | ||
| return cfengine_commands.show(args.hosts) | ||
| if args.command == "moduleinfo": | ||
| return cfengine_commands.moduleinfo(args.modules) | ||
| if args.command == "connect": | ||
| return cfengine_commands.connect(args.hosts) | ||
| raise UserError(f"Unknown command: '{args.command}'") | ||
|
|
||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.