From 21b84d669a5f7cf2922b8a2711d342a68140b227 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 08:12:39 +0000 Subject: [PATCH 01/14] [create-pull-request] automated change --- src/cfengine_cli/syntax-description.json | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/cfengine_cli/syntax-description.json b/src/cfengine_cli/syntax-description.json index 5d30c05..0bbb5b0 100644 --- a/src/cfengine_cli/syntax-description.json +++ b/src/cfengine_cli/syntax-description.json @@ -6786,15 +6786,30 @@ "string_replace": { "cached": false, "category": "data", - "collecting": false, + "collecting": true, + "maxArgs": 4, + "minArgs": 3, "parameters": [ { "description": "Source string", "range": ".*", "type": "string" }, - { "description": "String to replace", "range": ".*", "type": "string" }, - { "description": "Replacement string", "range": ".*", "type": "string" } + { + "description": "String or list of strings to replace", + "range": ".*", + "type": "string" + }, + { + "description": "Replacement string or list of strings", + "range": ".*", + "type": "string" + }, + { + "description": "Whether to expect strings or list of strings", + "range": "strings,lists", + "type": "option" + } ], "returnType": "string", "status": "normal", - "variadic": false + "variadic": true }, "string_reverse": { "cached": false, From 7ca735388818f96a4879c32f3f8fa0bd72dfec78 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Tue, 4 Aug 2026 14:01:17 +0200 Subject: [PATCH 02/14] Bumped cf-remote version & changed how parse/validation of `cf-remote` functions is done Ticket: ENT-14305 Changelog: Title Signed-off-by: Simon Halvorsen --- pyproject.toml | 2 +- .../cfengine_wrapper/arg_parse.py | 173 +++--------------- src/cfengine_cli/main.py | 77 +++----- uv.lock | 10 +- 4 files changed, 51 insertions(+), 211 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e52cfa0..ab85783 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" license = {file = "LICENSE"} requires-python = ">=3.10" dependencies = [ - "cf-remote>=0.7.3", + "cf-remote>=0.9.4", "cfbs>=5.5.0", "tree-sitter-cfengine>=1.1.12", "tree-sitter>=0.25", diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index 5292b29..3211243 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -1,27 +1,21 @@ import argparse +from cf_remote.args import ( + add_save_args, + add_deploy_args, + add_install_args, + add_uninstall_args, + add_spawn_args, + add_destroy_args, +) + def parse_wrapper_args(subp: argparse._SubParsersAction): - sp = subp.add_parser( - "save", help="Save host(s) with a group name to use in other commands" - ) - sp.add_argument( - "--role", - help="Role of the hosts", - choices=["hub", "hubs", "client", "clients"], - required=True, - ) - sp.add_argument( - "--name", - help="Name of the group of hosts (can be used in other commands)", - required=True, - ) - sp.add_argument( - "--hosts", - "-H", - help="SSH usernames and IPs for SSH and CFEngine in the form of user@ip", - required=True, + add_save_args( + subp.add_parser( + "save", help="Save host(s) with a group name to use in other commands" + ) ) sp = subp.add_parser( @@ -40,18 +34,13 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): help="""Build a policy set from a CFEngine Build project. A wrapper around the cfbs `build`-function.""", ) - sp = subp.add_parser( + + 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.""", ) - sp.add_argument("--hub", help="Hub(s) to deploy to", type=str) - sp.add_argument( - "masterfiles", - help="Policy-set location (tarball URL or local path to tarball / directory)", - type=str, - nargs="?", - ) + add_deploy_args(deploy_parser) install_parser = subp.add_parser( "install", @@ -64,78 +53,14 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): help="Specify version", type=str, ) - # install_parser._option_string_actions.get("--version").help = "absdfsf" - # TODO: Update cf-remote/cfbs to have more modular arg-parsing, then we can import - # and override any differences? technically illegal since _option_string_actions, - # but will save ~ 200-1000 loc depending on how much we import into cfengine-cli - - install_parser.add_argument( - "--edition", - "-E", - choices=["community", "enterprise"], - help="Enterprise or community packages", - type=str, - ) - install_parser.add_argument( - "--package", help="Local path to package or URL to download", type=str - ) - install_parser.add_argument( - "--hub-package", - help="Local path to package or URL to download for --hub", - type=str, - ) - install_parser.add_argument( - "--client-package", - help="Local path to package or URL to download for --clients", - type=str, - ) - install_parser.add_argument( - "--bootstrap", "-B", help="cf-agent --bootstrap argument", type=str - ) - install_parser.add_argument( - "--clients", "-c", help="Where to install client package", type=str - ) - install_parser.add_argument("--hub", help="Where to install hub package", type=str) - install_parser.add_argument( - "--demo", - help="Use defaults to make demos smoother (NOT secure)", - action="store_true", - ) - install_parser.add_argument( - "--call-collect", - help="Enable call collect in --demo def.json", - action="store_true", - ) - install_parser.add_argument( - "--remote-download", - help="Package will be downloaded directly to the target machine", - action="store_true", - ) - install_parser.add_argument( - "--trust-keys", - help="Comma-separated list of paths to keys hosts should trust" - + " (implies '--trust-server no' when boostraping)", - type=str, - ) - install_parser.add_argument( - "--insecure", - help="Ignore mismatching checksums when downloading urls", - action="store_true", - ) + add_install_args(install_parser) uninstall_parser = subp.add_parser( "uninstall", help="Uninstall CFEngine on the given hosts", description="A wrapper around the cf-remote `uninstall` function", ) - uninstall_parser.add_argument( - "--purge", help="Complete uninstallation", action="store_true" - ) - uninstall_parser.add_argument( - "--clients", "-c", help="Where to uninstall", type=str - ) - uninstall_parser.add_argument("--hub", help="Where to uninstall", type=str) - uninstall_parser.add_argument("--hosts", "-H", help="Where to uninstall", type=str) + add_uninstall_args(uninstall_parser) report_parser = subp.add_parser( "report", @@ -184,73 +109,19 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): "If omitted and multiple installations are found, you'll be prompted.", ) - sp = subp.add_parser( + spawn_parser = subp.add_parser( "spawn", help="Spawn hosts in the clouds", description="A wrapper around the cf-remote `spawn`-function", ) - sp.add_argument( - "--list-platforms", help="List supported platforms", action="store_true" - ) - sp.add_argument( - "--list-boxes", help="List installed vagrant boxes", action="store_true" - ) - sp.add_argument( - "--init-config", - help="Initialize configuration file for spawn functionality", - action="store_true", - ) - sp.add_argument("--platform", help="Platform or vagrant box to use", type=str) - sp.add_argument("--count", default=1, help="How many hosts to spawn", type=int) - sp.add_argument( - "--role", help="Role of the hosts", choices=["hub", "hubs", "client", "clients"] - ) - sp.add_argument( - "--name", help="Name of the group of hosts (can be used in other commands)" - ) - sp.add_argument( - "--append", - help="Append the new VMs to a pre-existing group", - action="store_true", - ) - sp.add_argument( - "--provider", - help="VM provider", - type=str, - default="aws", - choices=["aws", "gcp", "vagrant"], - ) - sp.add_argument("--cpus", help="Number of CPUs of the vagrant instances", type=int) - sp.add_argument( - "--sync-folder", - help="Root folder of synchronized folders of vagrant instance", - type=str, - ) - sp.add_argument( - "--provision", - help="full path to provision shell script for Vagrant VM", - type=str, - ) - sp.add_argument("--size", help="Size/type of the instances", type=str) - sp.add_argument( - "--network", help="network/subnet to assign the VMs to (GCP only)", type=str - ) - sp.add_argument( - "--no-public-ip", - help="No public IP needed (GCP only; WARNING: The VMs will only be accessible" - + " from some other VM in the same cloud/network!)", - action="store_true", - ) + add_spawn_args(spawn_parser) - dp = subp.add_parser( + destroy_parser = subp.add_parser( "destroy", help="Destroy hosts spawned in the clouds", description="A wrapper around the cf-remote `destroy`-function", ) - dp.add_argument( - "--all", help="Destroy all hosts spawned in the clouds", action="store_true" - ) - dp.add_argument("name", help="Name of the group of hosts to destroy", nargs="?") + add_destroy_args(destroy_parser) profile_parser = subp.add_parser( "profile", help="Parse CFEngine profiling output (cf-agent -Kp)" diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index 4dc83d5..1f4e9a2 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -7,7 +7,7 @@ from cf_remote import log from cf_remote.main import resolve_hosts -from cf_remote.utils import is_package_url, strip_user +from cf_remote.utils import strip_user, CFRExitError from cfengine_cli.cfengine_wrapper import cfengine_commands from cfengine_cli.cfengine_wrapper.arg_parse import parse_wrapper_args from cfengine_cli.version import cfengine_cli_version_string @@ -22,6 +22,14 @@ uninstall, ) from cf_remote.spawn import CFRUserError, Providers +from cf_remote.validate import ( + validate_edition_args, + validate_install_args, + validate_uninstall_args, + validate_spawn_args, + validate_deploy_args, + validate_destroy_args, +) from cfbs.utils import CFBSProgrammerError @@ -323,39 +331,19 @@ def run_command_with_args(args) -> int: def validate_args(args): if args.command == "dev" and args.dev_command is None: raise UserError("Missing subcommand - cfengine dev ") - if ( - args.command == "spawn" - and not args.list_platforms - and not args.init_config - and not args.list_boxes - ): - # The above options don't require any other options/arguments (TODO: - # --provider), but otherwise all have to be given - if not args.platform: - raise UserError("--platform needs to be specified") - if not args.count: - raise UserError("--count needs to be specified") - if not args.role: - raise UserError("--role needs to be specified") - if not args.name: - raise UserError("--name needs to be specified") + + if args.command == "spawn": + validate_spawn_args(args) if args.command == "destroy": - if not args.all and not args.name: - raise UserError("Either '--all' or 'NAME' must be specified for destroy") + validate_destroy_args(args) if args.all and args.name: raise UserError( "Only one of '--all' or 'NAME' may be specified for destruction" ) - if args.command in ["install"]: # , "packages", "list", "download"]: - if args.edition: - args.edition = args.edition.lower() - if args.edition == "core": - args.edition = "community" - if args.edition not in ["enterprise", "community"]: - raise UserError("--edition must be either community or enterprise") - else: - args.edition = "enterprise" + + if args.command == "install": + validate_edition_args(args) if "hosts" in args and args.hosts: log.debug(f"validate_args, hosts in args, args.hosts='{args.hosts}'") @@ -371,33 +359,14 @@ def validate_args(args): log.debug(f"validate_args, hubs in args, args.hub='{args.hub}'") args.hub = resolve_hosts(args.hub) - if args.command in ["uninstall"] and not (args.hosts or args.hub or args.clients): - raise UserError("Use --hosts, --hub or --clients to specify remote hosts") + if args.command == "uninstall": + validate_uninstall_args(args) if args.command == "install": - if args.call_collect and not args.demo: - raise UserError("--call-collect must be used with --demo") - if not args.clients and not args.hub: - raise UserError("Specify hosts using --hub and --clients") - if args.hub and args.clients and args.package: - raise UserError( - "Use --hub-package / --client-package instead to distinguish between hosts" - ) - if args.package and (args.hub_package or args.client_package): - raise UserError( - "--package cannot be used in combination with --hub-package / --client-package" - ) - if args.package and not is_package_url(args.package): - if not os.path.exists(os.path.expanduser(args.package)): - raise UserError("Package/directory '%s' does not exist" % args.package) - if args.hub_package and not is_package_url(args.hub_package): - if not os.path.isfile(args.hub_package): - raise UserError("Hub package '%s' does not exist" % args.hub_package) - if args.client_package and not is_package_url(args.client_package): - if not os.path.isfile(args.client_package): - raise UserError( - "Client package '%s' does not exist" % args.client_package - ) + validate_install_args(args) + + if args.command == "deploy": + validate_deploy_args(args) def _main(): @@ -416,7 +385,7 @@ def main(): exit_code = _main() assert type(exit_code) is int sys.exit(exit_code) - except (UserError, CFRUserError) as e: + except (UserError, CFRUserError, CFRExitError) as e: print(str(e)) sys.exit(-1) # Exceptions below are not expected, print extra info: diff --git a/uv.lock b/uv.lock index c104d77..0965478 100644 --- a/uv.lock +++ b/uv.lock @@ -34,14 +34,14 @@ wheels = [ [[package]] name = "cf-remote" -version = "0.9.0" +version = "0.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "apache-libcloud" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/46/d784f34cf508ffc560b7fe5975e421d6f0d0d9d97a156c72abca45aa65d7/cf_remote-0.9.0.tar.gz", hash = "sha256:c7f5e290a618cabd7281a9cd5357f261bb1edd077f296e9586c326d1638f676c", size = 67374, upload-time = "2026-06-12T17:29:19.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/97/bf02d8c78164bab0159aae505a9fbb7ec88703bb0035aef066aa47fd0df1/cf_remote-0.9.4.tar.gz", hash = "sha256:f00e5d852a83253f0398e4a490355ec2c00454f583977f5ace2f22453d4a8657", size = 69156, upload-time = "2026-08-07T10:40:26.015Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/68/8d2d95ccc080433d98de0003cb0c5018be8e10f2554ffbcf069d2497f179/cf_remote-0.9.0-py3-none-any.whl", hash = "sha256:e6c14d54d726dc91d9a8fc34cae48eeafd2e06ff71a14b55272fc3bc97d3b38b", size = 71449, upload-time = "2026-06-12T17:29:18.7Z" }, + { url = "https://files.pythonhosted.org/packages/ca/86/327d432911279e14203e13554213cb7f27677a60bb530468a33693cee2a1/cf_remote-0.9.4-py3-none-any.whl", hash = "sha256:d7856fd506762ef59bfb57ae5f52a2d8cbbcf475bfb9a9f7cb55bf1a6cf396fb", size = 73351, upload-time = "2026-08-07T10:40:24.884Z" }, ] [[package]] @@ -74,7 +74,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "cf-remote", specifier = ">=0.7.3" }, + { name = "cf-remote", specifier = ">=0.9.4" }, { name = "cfbs", specifier = ">=5.5.0" }, { name = "markdown-it-py", specifier = ">=3.0.0" }, { name = "pydantic", specifier = ">=2.12.5" }, @@ -208,7 +208,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ From beb5b49e25b70e2028e3d9a67a089210ff298a3d Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Thu, 6 Aug 2026 16:40:50 +0200 Subject: [PATCH 03/14] Added `cfengine show` to list your saved hosts --- src/cfengine_cli/cfengine_wrapper/arg_parse.py | 9 +++++++++ .../cfengine_wrapper/cfengine_commands.py | 11 ++++++++++- src/cfengine_cli/main.py | 2 ++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index 3211243..32af191 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -11,6 +11,15 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): + 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( diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index a688739..35dbd36 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -2,9 +2,10 @@ from cfbs.commands import build_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 cfengine_cli.utils import UserError @@ -242,3 +243,11 @@ def deploy(target: str | list[str] | None, masterfiles: str | None = None) -> in target = [target] hubs = [require_executable("cf-agent", h).location for h in (target or [])] or None return deploy_command(hubs, masterfiles) + + +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) diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index 1f4e9a2..20708cf 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -325,6 +325,8 @@ 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) raise UserError(f"Unknown command: '{args.command}'") From 67553845b85ca8046607d4b831f620a592738b58 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:36:22 +0000 Subject: [PATCH 04/14] [create-pull-request] automated change --- src/cfengine_cli/syntax-description.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/cfengine_cli/syntax-description.json b/src/cfengine_cli/syntax-description.json index 0bbb5b0..7dbc7aa 100644 --- a/src/cfengine_cli/syntax-description.json +++ b/src/cfengine_cli/syntax-description.json @@ -2141,6 +2141,27 @@ "type": "option", "visibility": "body" }, + "remount": { + "attribute": "remount", + "range": "true,false,yes,no,on,off", + "status": "normal", + "type": "option", + "visibility": "body" + }, + "remount_methods": { + "attribute": "remount_methods", + "range": "remount,unmount_mount", + "status": "normal", + "type": "olist", + "visibility": "body" + }, + "remount_timeout": { + "attribute": "remount_timeout", + "range": "0,99999999999", + "status": "normal", + "type": "int", + "visibility": "body" + }, "unmount": { "attribute": "unmount", "range": "true,false,yes,no,on,off", From f1b142b439a28ee217f700dc1616355a142ed726 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Wed, 29 Jul 2026 12:36:24 +0200 Subject: [PATCH 05/14] Added support to specify hub for automatic deploy&run after `cfengine build` --- .../cfengine_wrapper/arg_parse.py | 21 ++++++++++--- .../cfengine_wrapper/cfengine_commands.py | 31 +++++++++++++++---- .../cfengine_wrapper/cfengine_utils.py | 4 ++- src/cfengine_cli/main.py | 4 +-- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index 32af191..60fa253 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -38,18 +38,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", + 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", diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index 35dbd36..5c866bf 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -229,20 +229,39 @@ 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 (CFE-4704: https://northerntech.atlassian.net/browse/CFE-4704) + 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") + return error def show(target: list[str] | None = None) -> int: diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py b/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py index 4b16ee0..cf6b246 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py @@ -16,7 +16,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: diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index 20708cf..2d1969a 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -222,9 +222,9 @@ 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 == "format": return commands.format(args.files, args.line_length, args.check) if args.command == "lint": From 70c0ddc4b8f161a902f3a48353d1fb6be733a508 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Thu, 6 Aug 2026 17:37:01 +0200 Subject: [PATCH 06/14] Added `cfengine moduleinfo` to show module-information/status of a cfbs-project --- .../cfengine_wrapper/arg_parse.py | 12 +++++++++ .../cfengine_wrapper/cfengine_commands.py | 26 ++++++++++++++++--- src/cfengine_cli/main.py | 2 ++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index 60fa253..b3d2000 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -11,6 +11,18 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): + 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" ) diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index 5c866bf..d68fb64 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -1,6 +1,11 @@ import os -from cfbs.commands import build_command +from cfbs.utils import is_cfbs_repo +from cfbs.commands import ( + build_command, + info_command, + status_command, +) from cf_remote import log from cf_remote.commands import deploy as deploy_command, info from cf_remote.commands import destroy as destroy_command @@ -233,7 +238,11 @@ 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, non_interactive=non_interactive): + if prompt_yes_no( + "Deploy the built policy set now?", + default=True, + non_interactive=non_interactive, + ): return deploy(hub, None, non_interactive) return 0 @@ -258,7 +267,9 @@ def deploy( else: return deploy_command(hubs, masterfiles) - if prompt_yes_no("Run policy set now?", default=True, non_interactive=non_interactive): + 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") return error @@ -270,3 +281,12 @@ def show(target: list[str] | None = None) -> int: if isinstance(target, str): target = [target] return info(target) + + +def moduleinfo(modules: list[str]) -> int: + if modules != []: + return info_command(modules) + if not is_cfbs_repo(): + log.error("This is not a cfbs repo, to get started, type: cfengine init") + return 1 + return status_command() diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index 2d1969a..60e40dd 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -327,6 +327,8 @@ def run_command_with_args(args) -> int: return commands.up(args) if args.command == "show": return cfengine_commands.show(args.hosts) + if args.command == "moduleinfo": + return cfengine_commands.moduleinfo(args.modules) raise UserError(f"Unknown command: '{args.command}'") From a07a6fa2c21bd0c973fd180bd20aa644d7ecd90a Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Fri, 7 Aug 2026 11:39:46 +0200 Subject: [PATCH 07/14] Added `cfengine connect` to open a SSH-session to a saved host --- src/cfengine_cli/cfengine_wrapper/arg_parse.py | 9 +++++++++ src/cfengine_cli/cfengine_wrapper/cfengine_commands.py | 5 +++++ src/cfengine_cli/main.py | 2 ++ 3 files changed, 16 insertions(+) diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index b3d2000..ceab131 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -7,10 +7,19 @@ add_uninstall_args, add_spawn_args, add_destroy_args, + add_connect_args, ) def parse_wrapper_args(subp: argparse._SubParsersAction): + 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", diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index d68fb64..b3e4e49 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -12,6 +12,7 @@ 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 cfengine_cli.utils import UserError from cfengine_cli.cfengine_wrapper.cfengine_objects import ( @@ -290,3 +291,7 @@ def moduleinfo(modules: list[str]) -> int: log.error("This is not a cfbs repo, to get started, type: cfengine init") return 1 return status_command() + + +def connect(host) -> int: + return connect_cmd(host) diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index 60e40dd..b9912df 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -329,6 +329,8 @@ def run_command_with_args(args) -> int: 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}'") From d600fd98c7e42cafaf4d5b4bccf95e70839a4aa4 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Fri, 7 Aug 2026 14:12:50 +0200 Subject: [PATCH 08/14] Added `cfengine input` to set input for specified module --- src/cfengine_cli/cfengine_wrapper/arg_parse.py | 11 +++++++++++ .../cfengine_wrapper/cfengine_commands.py | 5 +++++ src/cfengine_cli/main.py | 2 ++ 3 files changed, 18 insertions(+) diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index ceab131..19d0d54 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -12,6 +12,17 @@ def parse_wrapper_args(subp: argparse._SubParsersAction): + 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", diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index b3e4e49..eb9812a 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -13,6 +13,7 @@ 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 from cfengine_cli.utils import UserError from cfengine_cli.cfengine_wrapper.cfengine_objects import ( @@ -295,3 +296,7 @@ def moduleinfo(modules: list[str]) -> int: def connect(host) -> int: return connect_cmd(host) + + +def cfbs_input(modules: list[str] | None = None) -> int: + return input_command(modules, "cfengine input") diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index b9912df..e0a85da 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -225,6 +225,8 @@ def run_command_with_args(args) -> int: return cfengine_commands.build(args.hub, args.non_interactive) if args.command == "deploy": return cfengine_commands.deploy(args.hub, args.masterfiles, args.non_interactive) + if args.command == "input": + return cfengine_commands.cfbs_input(args.module) if args.command == "format": return commands.format(args.files, args.line_length, args.check) if args.command == "lint": From ea3208577e072f090a472f89cf98d0f575e742cd Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Fri, 7 Aug 2026 14:52:54 +0200 Subject: [PATCH 09/14] Fixed help-descriptor for `cfengine run` --- src/cfengine_cli/cfengine_wrapper/arg_parse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index 19d0d54..9a30084 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -133,8 +133,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` From e99f80bef69e7ac5889c8cfc81c665254f91e230 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Fri, 7 Aug 2026 15:23:23 +0200 Subject: [PATCH 10/14] Added `cfengine add/remove/search/update` to add/remove/search for modules or update cfbs project --- .../cfengine_wrapper/arg_parse.py | 42 +++++++++++++++++++ .../cfengine_wrapper/cfengine_commands.py | 26 +++++++++++- src/cfengine_cli/main.py | 12 +++++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py index 9a30084..f72d4a8 100644 --- a/src/cfengine_cli/cfengine_wrapper/arg_parse.py +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -12,6 +12,48 @@ 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)", diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index eb9812a..707ac09 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -13,7 +13,13 @@ 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 +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 ( @@ -298,5 +304,21 @@ def connect(host) -> int: return connect_cmd(host) -def cfbs_input(modules: list[str] | None = None) -> int: +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) diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index e0a85da..c8e8c5a 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -224,9 +224,19 @@ def run_command_with_args(args) -> int: if args.command == "build": return cfengine_commands.build(args.hub, args.non_interactive) if args.command == "deploy": - return cfengine_commands.deploy(args.hub, args.masterfiles, args.non_interactive) + return cfengine_commands.deploy( + args.hub, args.masterfiles, args.non_interactive + ) if args.command == "input": 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": From fbb5abab7b8d9df2b50be3de270e49f936c2092b Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Fri, 7 Aug 2026 17:01:25 +0200 Subject: [PATCH 11/14] Changed selection strategy as to not resolve all hosts if --host HOST is specified Ticket: None Changelog: Title Signed-off-by: Simon Halvorsen --- .../cfengine_wrapper/cfengine_utils.py | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py b/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py index cf6b246..17e5ca3 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py @@ -1,3 +1,4 @@ +from collections import namedtuple import os import shutil import random @@ -105,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 + 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]: @@ -197,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})" ) From 6635f6cf41951db4a9780a03a9672a2ed2652dc1 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Mon, 10 Aug 2026 11:15:43 +0200 Subject: [PATCH 12/14] Added CHEATSHEET.md --- CHEATSHEET.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 122 insertions(+) create mode 100644 CHEATSHEET.md diff --git a/CHEATSHEET.md b/CHEATSHEET.md new file mode 100644 index 0000000..f6bff58 --- /dev/null +++ b/CHEATSHEET.md @@ -0,0 +1,120 @@ +# CFEngine CLI cheatsheet + +Quick reference for `cfengine COMMANDS` +For the full command list, run `cfengine help` or `cfengine COMMAND --help`. + +## Managing hosts + +```bash +# Save one or more hosts under a group name, for reuse in other commands +cfengine save --hosts 192.168.56.90 --role hub --name myhub + +# List all saved host-groups +cfengine show + +# Show details about a specific saved host (or group) +cfengine show --host myhub + +# Open an interactive SSH session to a saved host +cfengine connect --hosts myhub +``` + +## Managing cfbs modules + +These are thin wrappers around the equivalent `cfbs` subcommands, usable from anywhere inside a cfbs project. + +```bash +# Search the build-index for a module +cfengine search promise-type-git + +# Add module(s) to the current cfbs project +cfengine add promise-type-git + +# Set/update input.json for a module that takes input +cfengine input promise-type-git + +# Remove module(s) from the current cfbs project +cfengine remove promise-type-git + +# Update the current cfbs project (or specific modules) +cfengine update +cfengine update promise-type-git + +# Show status of the current cfbs project +cfengine moduleinfo + +# Show info about specific module(s) (does not require being inside a project) +cfengine moduleinfo promise-type-git +``` + +## Building and deploying policy sets + +```bash +# Build the policy set from a cfbs project (equivalent to `cfbs build`) +cfengine build + +# build -> deploy -> run on the given hub without extra prompts +cfengine build --hub myhub --non-interactive + +# Deploy an already-built policy set to a hub +cfengine deploy --hub myhub +``` + +## Running the CFEngine agent + +```bash +# Default: cf-agent -KIf update.cf && cf-agent -KIf +# If there is a local installation, this will be used +cfengine run + +# Run a specific policy file (resolved locally, or uploaded if not found remotely) +cfengine run /tmp/some_policy.cf + +# Run against a specific saved host, instead of being prompted +cfengine run --host myhub + +# Chain multiple commands, executed in sequence +cfengine run /tmp/some_policy.cf /tmp/other_policy.cf +``` + +## Initializing new projects + +```bash +# Build project on top of the default masterfiles (default, same as `cfbs init`) +cfengine init --policy-set + +# Initialize an example build project for working on a custom promise type (python) +cfengine init --promise-type + +# Initialize an exapmlte module project for build.cfengine.com (or internal use) +cfengine init --policy-module + +# Initialize an example policy module that takes input data +cfengine init --policy-module --with-input + +# Skip interactive prompts +cfengine init --promise-module --non-interactive +``` + +## Formatting and linting + +```bash +cfengine format +cfengine lint +cfengine lint main.cf +``` + +## Reporting + +```bash +# Refresh reporting data on all known hubs/clients (capped at 25 hosts), optional flag to run agent first. +cfengine report [--run-agent] + +# Only refresh a specific hub (and its clients, capped at 25), running the agent first +cfengine report --hub myhub --run-agent +``` + +--- + +See [README.md](./README.md) for installation and general usage, and [HACKING.md](./HACKING.md) for +contributing/development info. diff --git a/README.md b/README.md index 0d8df20..0248e33 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ To ignore the previously saved configuration and apply the current configuration cfengine up --reset config.yaml ``` +See [CHEATSHEET.md](./CHEATSHEET.md) for a fuller command reference. + ## Supported platforms and versions This tool will only support a limited number of platforms, it is not intended to run everywhere CFEngine runs. From eaf82d6a2d66bff15a0a5ada8815d8650850b261 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Mon, 10 Aug 2026 11:25:05 +0200 Subject: [PATCH 13/14] Added localhost/local known targets for build/deploy --- src/cfengine_cli/cfengine_wrapper/cfengine_commands.py | 6 +++++- src/cfengine_cli/cfengine_wrapper/cfengine_utils.py | 2 +- src/cfengine_cli/main.py | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index 707ac09..7615fd8 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -271,7 +271,11 @@ def deploy( # TODO/WOULD be nice: Deploy without run (CFE-4704: https://northerntech.atlassian.net/browse/CFE-4704) if hubs: - error = deploy_command(hubs.keys(), masterfiles) + # cf-remote functions use "localhost" (not "local" as it is here) + deploy_targets = [ + "localhost" if location == "local" else location for location in hubs + ] + error = deploy_command(deploy_targets, masterfiles) else: return deploy_command(hubs, masterfiles) diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py b/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py index 17e5ca3..07d22c9 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py @@ -111,7 +111,7 @@ def _hosts_with_info(role_filter=None): def _identities(binary_name: str) -> Iterator[_Id]: """local + every known host as (location, aliases), without connecting.""" - yield _Id("local", []) + yield _Id("local", ["localhost"]) for host, aliases in _known_hosts(None if binary_name == "cf-agent" else "hub"): yield _Id(host, aliases) diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index c8e8c5a..afe8050 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -375,7 +375,10 @@ def validate_args(args): ] if "hub" in args and args.hub: log.debug(f"validate_args, hubs in args, args.hub='{args.hub}'") - args.hub = resolve_hosts(args.hub) + if args.hub in ["local", "localhost"]: + args.hub = ["local"] + else: + args.hub = resolve_hosts(args.hub) if args.command == "uninstall": validate_uninstall_args(args) From 5057055ee2b67c2326fa3c5f36dff95dfeedd9a5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:31:26 +0000 Subject: [PATCH 14/14] [create-pull-request] automated change --- src/cfengine_cli/syntax-description.json | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/cfengine_cli/syntax-description.json b/src/cfengine_cli/syntax-description.json index 7dbc7aa..70789ce 100644 --- a/src/cfengine_cli/syntax-description.json +++ b/src/cfengine_cli/syntax-description.json @@ -4264,7 +4264,7 @@ "format": { "cached": false, "category": "data", - "collecting": false, + "collecting": true, "minArgs": 1, "parameters": [ { @@ -7091,6 +7091,26 @@ "status": "normal", "variadic": true }, + "validfiledata": { + "cached": false, + "category": "io", + "collecting": false, + "parameters": [ + { + "description": "File name to read", + "range": "\"?(/.*)", + "type": "string" + }, + { + "description": "Type of data to read", + "range": "CSV,YAML,JSON,ENV,auto", + "type": "option" + } + ], + "returnType": "context", + "status": "normal", + "variadic": false + }, "validjson": { "cached": false, "category": "data",