From 9594ad1a8da92e1d16870825b765c5103a5f3c70 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Mon, 13 Jul 2026 12:56:31 +0200 Subject: [PATCH] ENT-14118: Added cf-remotes `spawn` & `destroy` functionality to cfengine-cli Ticket: ENT-14118 Signed-off-by: Simon Halvorsen --- .../cfengine_wrapper/cfengine_commands.py | 11 +- src/cfengine_cli/main.py | 141 +++++++++++++++++- 2 files changed, 144 insertions(+), 8 deletions(-) diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index ba8327b..90a7d54 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -1,7 +1,6 @@ from cfbs.commands import build_command from cf_remote.commands import deploy as deploy_command from cf_remote.commands import install as install_command -from cf_remote.commands import spawn as spawn_command from cf_remote.commands import destroy as destroy_command from cfengine_cli.cfengine_wrapper.cfengine_utils import ( @@ -31,12 +30,10 @@ def install() -> int: # TODO ENT-14117 return install_command(None, None) -def spawn() -> int: # TODO ENT-14118 - return spawn_command(None, None, None, None) - - -def destroy() -> int: # TODO ENT-14118 - return destroy_command(None) +def destroy(groupname, del_all=False) -> int: + if del_all: + return destroy_command(None) + return destroy_command(groupname) def build() -> int: # TODO ENT-14119 diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index d0bccf7..e5db58d 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -10,6 +10,8 @@ from cfengine_cli.version import cfengine_cli_version_string from cfengine_cli import commands from cfengine_cli.utils import UserError +from cf_remote.commands import spawn, list_boxes, list_platforms, init_cloud_config +from cf_remote.spawn import CFRUserError, Providers from cfbs.utils import CFBSProgrammerError @@ -93,6 +95,66 @@ def _get_arg_parser(): "If omitted and multiple installations are found, you'll be prompted.", ) + sp = subp.add_parser("spawn", help="Spawn hosts in the clouds") + 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", + ) + + dp = subp.add_parser("destroy", help="Destroy hosts spawned in the clouds") + 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="?") + profile_parser = subp.add_parser( "profile", help="Parse CFEngine profiling output (cf-agent -Kp)" ) @@ -222,6 +284,59 @@ def run_command_with_args(args) -> int: return cfengine_commands.report(target=args.host) if args.command == "run": return cfengine_commands.run(*args.run_args, target=args.host) + if args.command == "spawn": + if args.list_platforms: + return list_platforms() + if args.list_boxes: + return list_boxes() + if args.init_config: + return init_cloud_config() + if args.name and "," in args.name: + raise UserError("Group --name may not contain commas") + if args.role and args.role.endswith("s"): + # role should be singular + args.role = args.role[:-1] + if args.provider == "gcp": + provider = Providers.GCP + elif args.provider == "aws": + provider = Providers.AWS + if args.network: + raise UserError("--network not supported for AWS") + if args.no_public_ip: + raise UserError("--no-public-ip not supported for AWS") + else: + assert args.provider == "vagrant" + provider = Providers.VAGRANT + + if provider != Providers.VAGRANT: + if args.cpus: + raise UserError(f"--cpus not supported for {args.provider}") + if args.sync_folder: + raise UserError(f"--sync-folder not supported for {args.provider}") + if args.provision: + raise UserError(f"--provision not supported for {args.provider}") + + if args.network and (args.network.count("/") != 1): + raise UserError( + "Invalid network specified, needs to be in the network/subnet format" + ) + + return spawn( + args.platform, + args.count, + args.role, + args.name, + provider=provider, + size=args.size, + network=args.network, + public_ip=not args.no_public_ip, + extend_group=args.append, + vagrant_cpus=args.cpus, + vagrant_sync_folder=args.sync_folder, + vagrant_provision=args.provision, + ) + if args.command == "destroy": + return cfengine_commands.destroy(args.name, del_all=args.all) if args.command == "dev": return commands.dev(args.dev_command, args) if args.command == "profile": @@ -234,6 +349,30 @@ 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 == "destroy": + if not args.all and not args.name: + raise UserError("Either '--all' or 'NAME' must be specified for destroy") + if args.all and args.name: + raise UserError( + "Only one of '--all' or 'NAME' may be specified for destruction" + ) def _main(): @@ -252,7 +391,7 @@ def main(): exit_code = _main() assert type(exit_code) is int sys.exit(exit_code) - except UserError as e: + except (UserError, CFRUserError) as e: print(str(e)) sys.exit(-1) # Exceptions below are not expected, print extra info: