diff --git a/src/cfengine_cli/cfengine_wrapper/arg_parse.py b/src/cfengine_cli/cfengine_wrapper/arg_parse.py new file mode 100644 index 0000000..4dbf1ef --- /dev/null +++ b/src/cfengine_cli/cfengine_wrapper/arg_parse.py @@ -0,0 +1,211 @@ +import argparse + + +def parse_wrapper_args(subp: argparse._SubParsersAction): + install_parser = subp.add_parser( + "install", + help="Install CFEngine on the given hosts", + description="A wrapper around the cf-remote `install` function", + ) + install_parser.add_argument( + "--version", + "-V", + 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", + ) + + 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) + + report_parser = subp.add_parser( + "report", + help="Run the agent and hub commands necessary to get new reporting data", + ) + report_parser.add_argument( + "--host", + type=str, + default=None, + help="Select which installation to use by name/IP (e.g. 'local' or '192.168.56.90'). " + "If omitted and multiple installations of cf-agent+cf-hub are found, you'll be prompted.", + ) + + 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", + epilog="""Examples: + `cfengine run` defaults to use `cf-agent -KIf update.cf && cf-agent -KI` + + Run can also be used directly on a specific file, e.g. + 'cfengine run /tmp/some_policy.cf' or 'cfengine run "-KIf /tmp/some_policy.cf"' + If no flags are present in the command, then -KIf will be automatically prepended. + + Multiple commands can also be run in sequence, such as: + 'cfengine run /tmp/some_policy.cf /tmp/some_other_policy.cf /tmp/and_another.cf' + Where all three files will be run in sequence, exiting on first fail + """, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + run_parser.add_argument( + "run_args", + nargs="*", + help="Command(s) to run with cf-agent", + ) + run_parser.add_argument( + "--host", + type=str, + default=None, + help="Select which installation of cf-agent to use by name/IP (e.g. 'local' or '192.168.56.90'). " + "If omitted and multiple installations are found, you'll be prompted.", + ) + + sp = 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", + ) + + dp = 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="?") + + profile_parser = subp.add_parser( + "profile", help="Parse CFEngine profiling output (cf-agent -Kp)" + ) + profile_parser.add_argument( + "profiling_input", help="Path to the profiling input file" + ) + profile_parser.add_argument("--top", type=int, default=10) + profile_parser.add_argument("--bundles", action="store_true") + profile_parser.add_argument("--promises", action="store_true") + profile_parser.add_argument("--functions", action="store_true") + profile_parser.add_argument( + "--flamegraph", type=str, help="Generate input file for ./flamegraph.pl" + ) diff --git a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py index 90a7d54..a8eb843 100644 --- a/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +++ b/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py @@ -1,6 +1,5 @@ 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 destroy as destroy_command from cfengine_cli.cfengine_wrapper.cfengine_utils import ( @@ -26,10 +25,6 @@ def run(*args, target: str | None = None) -> int: return agent.run("-KIf update.cf", "-KI") -def install() -> int: # TODO ENT-14117 - return install_command(None, None) - - def destroy(groupname, del_all=False) -> int: if del_all: return destroy_command(None) diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index e5db58d..a588d5d 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -6,11 +6,21 @@ import subprocess from cf_remote import log +from cf_remote.main import resolve_hosts +from cf_remote.utils import is_package_url, strip_user 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 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.commands import ( + spawn, + list_boxes, + list_platforms, + init_cloud_config, + install, + uninstall, +) from cf_remote.spawn import CFRUserError, Providers from cfbs.utils import CFBSProgrammerError @@ -40,6 +50,9 @@ def _get_arg_parser(): % os.path.basename(sys.argv[0]) ) subp = ap.add_subparsers(dest="command", title=command_help_hint) + parse_wrapper_args( + subp + ) # The flags for run/report/spawn/destroy/...all wrapper functions subp.add_parser("help", help="Print help information") subp.add_parser( @@ -68,106 +81,6 @@ def _get_arg_parser(): help="Lint based on a user given syntax description", ) lnt.add_argument("files", nargs="*", help="Files to lint") - report_parser = subp.add_parser( - "report", - help="Run the agent and hub commands necessary to get new reporting data", - ) - report_parser.add_argument( - "--host", - type=str, - default=None, - help="Select which installation to use by name/IP (e.g. 'local' or '192.168.56.90'). " - "If omitted and multiple installations of cf-agent+cf-hub are found, you'll be prompted.", - ) - run_parser = subp.add_parser( - "run", help="Run the CFEngine agent, fetching, evaluating, and enforcing policy" - ) - run_parser.add_argument( - "run_args", - nargs="*", - help="Command(s) to run with cf-agent", - ) - run_parser.add_argument( - "--host", - type=str, - default=None, - help="Select which installation of cf-agent to use by name/IP (e.g. 'local' or '192.168.56.90'). " - "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)" - ) - profile_parser.add_argument( - "profiling_input", help="Path to the profiling input file" - ) - profile_parser.add_argument("--top", type=int, default=10) - profile_parser.add_argument("--bundles", action="store_true") - profile_parser.add_argument("--promises", action="store_true") - profile_parser.add_argument("--functions", action="store_true") - profile_parser.add_argument( - "--flamegraph", type=str, help="Generate input file for ./flamegraph.pl" - ) dev_parser = subp.add_parser( "dev", help="Utilities intended for developers / maintainers of CFEngine" @@ -282,6 +195,32 @@ def run_command_with_args(args) -> int: ) if args.command == "report": return cfengine_commands.report(target=args.host) + + if args.command == "install": + print(args) + if args.trust_keys: + trust_keys = args.trust_keys.split(",") + else: + trust_keys = None + + return install( + args.hub, + args.clients, + package=args.package, + bootstrap=args.bootstrap, + hub_package=args.hub_package, + client_package=args.client_package, + version=args.version, + demo=args.demo, + call_collect=args.call_collect, + edition=args.edition, + remote_download=args.remote_download, + trust_keys=trust_keys, + insecure=args.insecure, + ) + elif args.command == "uninstall": + all_hosts = (args.hosts or []) + (args.hub or []) + (args.clients or []) + return uninstall(all_hosts, purge=args.purge) if args.command == "run": return cfengine_commands.run(*args.run_args, target=args.host) if args.command == "spawn": @@ -373,6 +312,56 @@ def validate_args(args): 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 "hosts" in args and args.hosts: + log.debug("validate_args, hosts in args, args.hosts='{}'".format(args.hosts)) + args.hosts = resolve_hosts(args.hosts) + if "clients" in args and args.clients: + args.clients = resolve_hosts(args.clients) + if "bootstrap" in args and args.bootstrap: + args.bootstrap = [ + strip_user(host_info) + for host_info in resolve_hosts(args.bootstrap, bootstrap_ips=True) + ] + if "hub" in args and 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 == "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 + ) def _main():