diff --git a/cf_remote/args.py b/cf_remote/args.py new file mode 100644 index 0000000..da480eb --- /dev/null +++ b/cf_remote/args.py @@ -0,0 +1,353 @@ +import argparse +import os +import sys + +from cf_remote.utils import cache + + +def add_info_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--hosts", "-H", help="Which hosts to get info for", type=str, required=True + ) + + +def add_install_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--edition", + "-E", + choices=["community", "enterprise"], + help="Enterprise or community packages", + type=str, + ) + sp.add_argument( + "--package", help="Local path to package or URL to download", type=str + ) + sp.add_argument( + "--hub-package", + help="Local path to package or URL to download for --hub", + type=str, + ) + sp.add_argument( + "--client-package", + help="Local path to package or URL to download for --clients", + type=str, + ) + sp.add_argument("--bootstrap", "-B", help="cf-agent --bootstrap argument", type=str) + sp.add_argument("--clients", "-c", help="Where to install client package", type=str) + sp.add_argument("--hub", help="Where to install hub package", type=str) + sp.add_argument( + "--demo", + help="Use defaults to make demos smoother (NOT secure)", + action="store_true", + ) + sp.add_argument( + "--call-collect", + help="Enable call collect in --demo def.json", + action="store_true", + ) + sp.add_argument( + "--remote-download", + help="Package will be downloaded directly to the target machine", + action="store_true", + ) + sp.add_argument( + "--trust-keys", + help="Comma-separated list of paths to keys hosts should trust" + + " (implies '--trust-server no' when boostraping)", + type=str, + ) + sp.add_argument( + "--insecure", + help="Ignore mismatching checksums when downloading urls", + action="store_true", + ) + + +def add_uninstall_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument("--purge", help="Complete uninstallation", action="store_true") + sp.add_argument("--clients", "-c", help="Where to uninstall", type=str) + sp.add_argument("--hub", help="Where to uninstall", type=str) + sp.add_argument("--hosts", "-H", help="Where to uninstall", type=str) + + +def add_packages_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--edition", + "-E", + choices=["community", "enterprise"], + help="Enterprise or community packages", + type=str, + ) + sp.add_argument("tags", metavar="TAG", nargs="*") + + +def add_list_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--edition", + "-E", + choices=["community", "enterprise"], + help="Enterprise or community packages", + type=str, + ) + sp.add_argument("tags", metavar="TAG", nargs="*") + sp.add_argument( + "--allow-expired", help="Also lists expired packages", action="store_true" + ) + + +def add_download_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--edition", + "-E", + choices=["community", "enterprise"], + help="Enterprise or community packages", + type=str, + ) + sp.add_argument("tags", metavar="TAG", nargs="*") + sp.add_argument("--output-dir", "-o", help="Where to download", type=str) + sp.add_argument( + "--insecure", + help="Ignore mismatching checksums when downloading urls", + action="store_true", + ) + sp.add_argument( + "--allow-expired", help="Allow expired packages", action="store_true" + ) + + +def add_run_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--hosts", + "-H", + help="Which hosts to run the command on", + type=str, + required=True, + ) + sp.add_argument( + "--raw", help="Print only output of command itself", action="store_true" + ) + sp.add_argument( + "remote_command", + help="Command to execute on remote host (including args)", + type=str, + nargs=1, + ) + + +def add_save_args(sp: argparse.ArgumentParser) -> None: + 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, + ) + + +def add_sudo_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--hosts", + "-H", + help="Which hosts to run the command on", + type=str, + required=True, + ) + sp.add_argument( + "--raw", help="Print only output of command itself", action="store_true" + ) + sp.add_argument( + "remote_command", + help="Command to execute on remote host (including args)", + type=str, + nargs=1, + ) + + +def add_scp_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--hosts", "-H", help="Which hosts to copy the file to", type=str, required=True + ) + sp.add_argument("args", help="Arguments", type=str, nargs="*") + + +def add_spawn_args(sp: argparse.ArgumentParser) -> None: + 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", + ) + # TODO: --region (optional) + + +def add_show_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--ansible-inventory", + help="Print Ansible inventory with spawned hosts", + action="store_true", + ) + + +def add_destroy_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--all", help="Destroy all hosts spawned in the clouds", action="store_true" + ) + sp.add_argument("name", help="Name of the group of hosts to destroy", nargs="?") + + +def add_deploy_args(sp: argparse.ArgumentParser) -> None: + 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="?", + ) + + +def add_agent_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--hosts", + "-H", + help="Which hosts to run cf-agent from", + type=str, + required=True, + ) + sp.add_argument("--bootstrap", "-B", help="Which hub to bootstrap to", type=str) + + +def add_connect_args(sp: argparse.ArgumentParser) -> None: + sp.add_argument( + "--hosts", "-H", help="Host to open the shell on", type=str, required=True + ) + + +@cache +def get_arg_parser() -> argparse.ArgumentParser: + ap = argparse.ArgumentParser( + description="Spooky CFEngine at a distance", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + ap.add_argument( + "--log-level", + help="Specify level of logging: NONE, DEBUG, INFO, WARNING, ERROR, or CRITICAL", + type=str, + default="WARNING", + ) + ap.add_argument( + "--version", + "-V", + help="Print or specify version", + nargs="?", + type=str, + const=True, + ) + + command_help_hint = ( + "Commands (use %s COMMAND --help to get more info)" + % os.path.basename(sys.argv[0]) + ) + subp = ap.add_subparsers(dest="command", title=command_help_hint) + + add_info_args(subp.add_parser("info", help="Get info about the given hosts")) + add_install_args( + subp.add_parser("install", help="Install CFEngine on the given hosts") + ) + add_uninstall_args( + subp.add_parser("uninstall", help="Uninstall CFEngine on the given hosts") + ) + add_packages_args( + subp.add_parser("packages", help="Get info about available packages") + ) + add_list_args( + subp.add_parser("list", help="List CFEngine packages available for download") + ) + add_download_args(subp.add_parser("download", help="Download CFEngine packages")) + add_run_args( + subp.add_parser( + "run", help="Run the command given as arguments on the given hosts" + ) + ) + add_save_args( + subp.add_parser( + "save", help="Save host(s) with a group name to use in other commands" + ) + ) + add_sudo_args( + subp.add_parser( + "sudo", + help="Run the command given as arguments on the given hosts with 'sudo'", + ) + ) + add_scp_args(subp.add_parser("scp", help="Copy the given file to the given hosts")) + add_spawn_args(subp.add_parser("spawn", help="Spawn hosts in the clouds")) + add_show_args( + subp.add_parser("show", help="Show hosts spawned by or added to cf-remote") + ) + add_destroy_args( + subp.add_parser("destroy", help="Destroy hosts spawned in the clouds") + ) + add_deploy_args( + subp.add_parser("deploy", help="Deploy policy-set (masterfiles) to hub") + ) + add_agent_args(subp.add_parser("agent", help="Run cf-agent")) + add_connect_args(subp.add_parser("connect", help="Opens interactive ssh shell")) + + return ap diff --git a/cf_remote/main.py b/cf_remote/main.py index 47615df..403f92a 100644 --- a/cf_remote/main.py +++ b/cf_remote/main.py @@ -1,4 +1,3 @@ -import argparse import os import sys import re @@ -7,6 +6,7 @@ from cf_remote import log from cf_remote import version from cf_remote import commands, paths +from cf_remote.args import get_arg_parser from cf_remote.utils import ( CFRExitError, CFRProgrammerError, @@ -16,9 +16,17 @@ is_file_string, migrate_config_paths, ) -from cf_remote.utils import strip_user, read_json, is_package_url, cache +from cf_remote.utils import strip_user, read_json from cf_remote.packages import Releases from cf_remote.spawn import Providers +from cf_remote.validate import ( + validate_edition_args, + validate_install_args, + validate_uninstall_args, + validate_spawn_args, + validate_destroy_args, + validate_deploy_args, +) def print_version_info(): @@ -28,304 +36,8 @@ def print_version_info(): print(releases) -@cache -def _get_arg_parser(): - ap = argparse.ArgumentParser( - description="Spooky CFEngine at a distance", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - ap.add_argument( - "--log-level", - help="Specify level of logging: NONE, DEBUG, INFO, WARNING, ERROR, or CRITICAL", - type=str, - default="WARNING", - ) - ap.add_argument( - "--version", - "-V", - help="Print or specify version", - nargs="?", - type=str, - const=True, - ) - - command_help_hint = ( - "Commands (use %s COMMAND --help to get more info)" - % os.path.basename(sys.argv[0]) - ) - subp = ap.add_subparsers(dest="command", title=command_help_hint) - - sp = subp.add_parser("info", help="Get info about the given hosts") - sp.add_argument( - "--hosts", "-H", help="Which hosts to get info for", type=str, required=True - ) - - sp = subp.add_parser("install", help="Install CFEngine on the given hosts") - sp.add_argument( - "--edition", - "-E", - choices=["community", "enterprise"], - help="Enterprise or community packages", - type=str, - ) - sp.add_argument( - "--package", help="Local path to package or URL to download", type=str - ) - sp.add_argument( - "--hub-package", - help="Local path to package or URL to download for --hub", - type=str, - ) - sp.add_argument( - "--client-package", - help="Local path to package or URL to download for --clients", - type=str, - ) - sp.add_argument("--bootstrap", "-B", help="cf-agent --bootstrap argument", type=str) - sp.add_argument("--clients", "-c", help="Where to install client package", type=str) - sp.add_argument("--hub", help="Where to install hub package", type=str) - sp.add_argument( - "--demo", - help="Use defaults to make demos smoother (NOT secure)", - action="store_true", - ) - sp.add_argument( - "--call-collect", - help="Enable call collect in --demo def.json", - action="store_true", - ) - sp.add_argument( - "--remote-download", - help="Package will be downloaded directly to the target machine", - action="store_true", - ) - sp.add_argument( - "--trust-keys", - help="Comma-separated list of paths to keys hosts should trust" - + " (implies '--trust-server no' when boostraping)", - type=str, - ) - sp.add_argument( - "--insecure", - help="Ignore mismatching checksums when downloading urls", - action="store_true", - ) - - sp = subp.add_parser("uninstall", help="Uninstall CFEngine on the given hosts") - sp.add_argument("--purge", help="Complete uninstallation", action="store_true") - sp.add_argument("--clients", "-c", help="Where to uninstall", type=str) - sp.add_argument("--hub", help="Where to uninstall", type=str) - sp.add_argument("--hosts", "-H", help="Where to uninstall", type=str) - - sp = subp.add_parser("packages", help="Get info about available packages") - sp.add_argument( - "--edition", - "-E", - choices=["community", "enterprise"], - help="Enterprise or community packages", - type=str, - ) - sp.add_argument("tags", metavar="TAG", nargs="*") - - sp = subp.add_parser("list", help="List CFEngine packages available for download") - sp.add_argument( - "--edition", - "-E", - choices=["community", "enterprise"], - help="Enterprise or community packages", - type=str, - ) - sp.add_argument("tags", metavar="TAG", nargs="*") - - sp.add_argument( - "--allow-expired", help="Also lists expired packages", action="store_true" - ) - - sp = subp.add_parser("download", help="Download CFEngine packages") - sp.add_argument( - "--edition", - "-E", - choices=["community", "enterprise"], - help="Enterprise or community packages", - type=str, - ) - sp.add_argument("tags", metavar="TAG", nargs="*") - - sp.add_argument("--output-dir", "-o", help="Where to download", type=str) - - sp.add_argument( - "--insecure", - help="Ignore mismatching checksums when downloading urls", - action="store_true", - ) - sp.add_argument( - "--allow-expired", help="Allow expired packages", action="store_true" - ) - - sp = subp.add_parser( - "run", help="Run the command given as arguments on the given hosts" - ) - sp.add_argument( - "--hosts", - "-H", - help="Which hosts to run the command on", - type=str, - required=True, - ) - sp.add_argument( - "--raw", help="Print only output of command itself", action="store_true" - ) - sp.add_argument( - "remote_command", - help="Command to execute on remote host (including args)", - type=str, - nargs=1, - ) - - 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, - ) - - sp = subp.add_parser( - "sudo", help="Run the command given as arguments on the given hosts with 'sudo'" - ) - sp.add_argument( - "--hosts", - "-H", - help="Which hosts to run the command on", - type=str, - required=True, - ) - sp.add_argument( - "--raw", help="Print only output of command itself", action="store_true" - ) - sp.add_argument( - "remote_command", - help="Command to execute on remote host (including args)", - type=str, - nargs=1, - ) - - sp = subp.add_parser("scp", help="Copy the given file to the given hosts") - sp.add_argument( - "--hosts", "-H", help="Which hosts to copy the file to", type=str, required=True - ) - sp.add_argument("args", help="Arguments", type=str, nargs="*") - - 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", - ) - # TODO: --region (optional) - - sp = subp.add_parser("show", help="Show hosts spawned by or added to cf-remote") - sp = sp.add_argument( - "--ansible-inventory", - help="Print Ansible inventory with spawned hosts", - 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="?") - - sp = subp.add_parser("deploy", help="Deploy policy-set (masterfiles) to hub") - 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="?", - ) - sp = subp.add_parser("agent", help="Run cf-agent") - sp.add_argument( - "--hosts", - "-H", - help="Which hosts to run cf-agent from", - type=str, - required=True, - ) - sp.add_argument("--bootstrap", "-B", help="Which hub to bootstrap to", type=str) - - sp = subp.add_parser("connect", help="Opens interactive ssh shell") - sp.add_argument( - "--hosts", "-H", help="Host to open the shell on", type=str, required=True - ) - - return ap - - def get_args(): - ap = _get_arg_parser() + ap = get_arg_parser() args = ap.parse_args() return args @@ -464,44 +176,13 @@ def run_command_with_args(command, args) -> int: def validate_command(command, args): if 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 CFRExitError("--edition must be either community or enterprise") - else: - args.edition = "enterprise" + validate_edition_args(args) - if command in ["uninstall"] and not (args.hosts or args.hub or args.clients): - raise CFRExitError("Use --hosts, --hub or --clients to specify remote hosts") + if command == "uninstall": + validate_uninstall_args(args) if command == "install": - if args.call_collect and not args.demo: - raise CFRExitError("--call-collect must be used with --demo") - if not args.clients and not args.hub: - raise CFRExitError("Specify hosts using --hub and --clients") - if args.hub and args.clients and args.package: - raise CFRExitError( - "Use --hub-package / --client-package instead to distinguish between hosts" - ) - if args.package and (args.hub_package or args.client_package): - raise CFRExitError( - "--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 CFRExitError( - "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 CFRExitError("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 CFRExitError( - "Client package '%s' does not exist" % args.client_package - ) + validate_install_args(args) if command in ["sudo", "run"]: if len(args.remote_command) != 1: @@ -510,36 +191,14 @@ def validate_command(command, args): ) args.remote_command = args.remote_command[0] - if ( - 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 CFRExitError("--platform needs to be specified") - if not args.count: - raise CFRExitError("--count needs to be specified") - if not args.role: - raise CFRExitError("--role needs to be specified") - if not args.name: - raise CFRExitError("--name needs to be specified") + if command == "spawn": + validate_spawn_args(args) if command == "destroy": - if not args.all and not args.name: - raise CFRExitError("One of --all or NAME required for destroy") + validate_destroy_args(args) - if command == "deploy" and args.masterfiles: - masterfiles = args.masterfiles - if masterfiles.startswith(("http://", "https://")): - if not masterfiles.endswith((".tgz", ".tar.gz")): - raise CFRExitError( - "masterfiles URL must be to a gzipped tarball (.tgz or .tar.gz)" - ) - elif not os.path.exists(masterfiles): - raise CFRExitError("'%s' does not exist" % masterfiles) + if command == "deploy": + validate_deploy_args(args) def is_in_cloud_state(name): @@ -677,7 +336,7 @@ def validate_args(args): args.hub = resolve_hosts(args.hub) if not args.command: - _get_arg_parser().print_help() + get_arg_parser().print_help() raise CFRExitError("Invalid or missing command") args.command = args.command.strip() validate_command(args.command, args) diff --git a/cf_remote/validate.py b/cf_remote/validate.py new file mode 100644 index 0000000..a127ccc --- /dev/null +++ b/cf_remote/validate.py @@ -0,0 +1,78 @@ +import os + +from cf_remote.utils import CFRExitError, is_package_url + + +def validate_edition_args(args) -> None: + if args.edition: + args.edition = args.edition.lower() + if args.edition == "core": + args.edition = "community" + if args.edition not in ["enterprise", "community"]: + raise CFRExitError("--edition must be either community or enterprise") + else: + args.edition = "enterprise" + + +def validate_install_args(args) -> None: + if args.call_collect and not args.demo: + raise CFRExitError("--call-collect must be used with --demo") + if not args.clients and not args.hub: + raise CFRExitError("Specify hosts using --hub and --clients") + if args.hub and args.clients and args.package: + raise CFRExitError( + "Use --hub-package / --client-package instead to distinguish between hosts" + ) + if args.package and (args.hub_package or args.client_package): + raise CFRExitError( + "--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 CFRExitError("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 CFRExitError("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 CFRExitError( + "Client package '%s' does not exist" % args.client_package + ) + + +def validate_uninstall_args(args) -> None: + if not (args.hosts or args.hub or args.clients): + raise CFRExitError("Use --hosts, --hub or --clients to specify remote hosts") + + +def validate_spawn_args(args) -> None: + if args.list_platforms or args.init_config or args.list_boxes: + # The above options don't require any other options/arguments (TODO: + # --provider), but otherwise all have to be given + return + if not args.platform: + raise CFRExitError("--platform needs to be specified") + if not args.count: + raise CFRExitError("--count needs to be specified") + if not args.role: + raise CFRExitError("--role needs to be specified") + if not args.name: + raise CFRExitError("--name needs to be specified") + + +def validate_destroy_args(args) -> None: + if not args.all and not args.name: + raise CFRExitError("One of --all or NAME required for destroy") + + +def validate_deploy_args(args) -> None: + if not args.masterfiles: + return + masterfiles = args.masterfiles + if masterfiles.startswith(("http://", "https://")): + if not masterfiles.endswith((".tgz", ".tar.gz")): + raise CFRExitError( + "masterfiles URL must be to a gzipped tarball (.tgz or .tar.gz)" + ) + elif not os.path.exists(masterfiles): + raise CFRExitError("'%s' does not exist" % masterfiles)