certbot/letsencrypt/cli.py

386 lines
13 KiB
Python
Raw Normal View History

2015-04-22 02:23:25 -04:00
"""Let's Encrypt CLI."""
2015-03-30 08:34:22 -04:00
# TODO: Sanity check all input. Be sure to avoid shell code etc...
import argparse
import logging
2015-04-22 03:22:15 -04:00
import os
import sys
import configargparse
2014-12-17 06:02:15 -05:00
import zope.component
2015-03-26 18:00:00 -04:00
import zope.interface.exceptions
import zope.interface.verify
2014-12-17 06:02:15 -05:00
import letsencrypt
2015-01-31 06:28:33 -05:00
2015-05-10 08:25:29 -04:00
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import constants
from letsencrypt import client
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt import log
from letsencrypt.display import util as display_util
from letsencrypt.display import ops as display_ops
from letsencrypt.plugins import disco as plugins_disco
2015-03-30 08:34:22 -04:00
def _account_init(args, config):
le_util.make_or_verify_dir(
config.config_dir, constants.CONFIG_DIRS_MODE, os.geteuid())
# Prepare for init of Client
if args.email is None:
return client.determine_account(config)
else:
try:
# The way to get the default would be args.email = ""
# First try existing account
return account.Account.from_existing_account(config, args.email)
except errors.LetsEncryptClientError:
try:
# Try to make an account based on the email address
return account.Account.from_email(config, args.email)
except errors.LetsEncryptClientError:
return None
def _common_run(args, config, acc, authenticator, installer):
2015-03-30 08:34:22 -04:00
if args.domains is None:
doms = display_ops.choose_names(installer)
else:
doms = args.domains
if not doms:
2015-05-02 04:53:06 -04:00
sys.exit("Please specify --domains, or --installer that will "
"help in domain names autodiscovery")
acme = client.Client(config, acc, authenticator, installer)
2015-03-26 18:00:00 -04:00
2015-03-30 08:34:22 -04:00
# Validate the key and csr
client.validate_key_csr(acc.key)
if authenticator is not None:
if acc.regr is None:
try:
acme.register()
except errors.LetsEncryptClientError:
2015-05-04 04:26:08 -04:00
sys.exit("Unable to register an account with ACME server")
2015-03-26 18:00:00 -04:00
2015-05-02 04:53:06 -04:00
return acme, doms
2015-03-30 08:34:22 -04:00
2015-05-02 04:53:06 -04:00
def run(args, config, plugins):
2015-03-30 08:34:22 -04:00
"""Obtain a certificate and install."""
acc = _account_init(args, config)
if acc is None:
return None
2015-03-30 08:34:22 -04:00
if args.configurator is not None and (args.installer is not None or
args.authenticator is not None):
return ("Either --configurator or --authenticator/--installer"
"pair, but not both, is allowed")
if args.authenticator is not None or args.installer is not None:
installer = display_ops.pick_installer(
2015-05-02 04:53:06 -04:00
config, args.installer, plugins)
authenticator = display_ops.pick_authenticator(
2015-05-02 04:53:06 -04:00
config, args.authenticator, plugins)
2015-03-30 08:34:22 -04:00
else:
2015-05-02 04:53:06 -04:00
# TODO: this assume that user doesn't want to pick authenticator
# and installer separately...
authenticator = installer = display_ops.pick_configurator(
2015-05-02 04:53:06 -04:00
config, args.configurator, plugins)
2015-03-30 08:34:22 -04:00
if installer is None or authenticator is None:
return "Configurator could not be determined"
2015-05-02 04:53:06 -04:00
acme, doms = _common_run(args, config, acc, authenticator, installer)
# TODO: Handle errors from _common_run?
lineage = acme.obtain_and_enroll_certificate(doms, authenticator,
installer)
if not lineage:
return "Certificate could not be obtained"
acme.deploy_certificate(doms, lineage)
2015-03-30 08:34:22 -04:00
acme.enhance_config(doms, args.redirect)
2015-05-02 04:53:06 -04:00
def auth(args, config, plugins):
2015-03-30 08:34:22 -04:00
"""Obtain a certificate (no install)."""
# XXX: Update for renewer / RenewableCert
acc = _account_init(args, config)
if acc is None:
return None
authenticator = display_ops.pick_authenticator(
config, args.authenticator, plugins)
2015-03-30 08:34:22 -04:00
if authenticator is None:
return "Authenticator could not be determined"
if args.installer is not None:
installer = display_ops.pick_installer(config, args.installer, plugins)
2015-03-30 08:34:22 -04:00
else:
installer = None
# TODO: Handle errors from _common_run?
2015-05-02 04:53:06 -04:00
acme, doms = _common_run(
2015-05-04 04:26:08 -04:00
args, config, acc, authenticator=authenticator, installer=installer)
if not acme.obtain_and_enroll_certificate(doms, authenticator, installer):
return "Certificate could not be obtained"
2015-03-30 08:34:22 -04:00
2015-05-02 04:53:06 -04:00
def install(args, config, plugins):
2015-03-30 08:34:22 -04:00
"""Install (no auth)."""
# XXX: Update for renewer/RenewableCert
2015-05-02 04:53:06 -04:00
acc = _account_init(args, config)
if acc is None:
return None
installer = display_ops.pick_installer(config, args.installer, plugins)
2015-03-30 08:34:22 -04:00
if installer is None:
return "Installer could not be determined"
2015-05-02 04:53:06 -04:00
acme, doms = _common_run(
args, config, acc, authenticator=None, installer=installer)
assert args.cert_path is not None
2015-05-11 18:02:02 -04:00
# XXX: This API has changed as a result of RenewableCert!
# acme.deploy_certificate(doms, acc.key, args.cert_path, args.chain_path)
2015-03-30 08:34:22 -04:00
acme.enhance_config(doms, args.redirect)
2015-05-04 04:26:08 -04:00
def revoke(args, unused_config, unused_plugins):
2015-03-30 08:34:22 -04:00
"""Revoke."""
if args.rev_cert is None and args.rev_key is None:
return "At least one of --certificate or --key is required"
# This depends on the renewal config and cannot be completed yet.
zope.component.getUtility(interfaces.IDisplay).notification(
"Revocation is not available with the new Boulder server yet.")
#client.revoke(args.installer, config, plugins, args.no_confirm,
# args.rev_cert, args.rev_key)
2015-03-30 08:34:22 -04:00
2015-05-02 04:53:06 -04:00
def rollback(args, config, plugins):
2015-03-30 08:34:22 -04:00
"""Rollback."""
client.rollback(args.installer, args.checkpoints, config, plugins)
2015-03-30 08:34:22 -04:00
2015-05-04 04:26:08 -04:00
def config_changes(unused_args, config, unused_plugins):
2015-03-30 08:34:22 -04:00
"""View config changes.
View checkpoints and associated configuration changes.
"""
2015-05-04 04:26:08 -04:00
client.view_config_changes(config)
2015-03-30 08:34:22 -04:00
def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print
2015-03-30 08:34:22 -04:00
"""List plugins."""
2015-05-04 08:46:24 -04:00
logging.debug("Expected interfaces: %s", args.ifaces)
2015-03-30 08:34:22 -04:00
ifaces = [] if args.ifaces is None else args.ifaces
2015-05-04 08:46:24 -04:00
filtered = plugins.ifaces(ifaces)
logging.debug("Filtered plugins: %r", filtered)
2015-03-30 08:34:22 -04:00
if not args.init and not args.prepare:
print str(filtered)
return
2015-03-30 08:34:22 -04:00
filtered.init(config)
verified = filtered.verify(ifaces)
logging.debug("Verified plugins: %r", verified)
2015-03-30 08:34:22 -04:00
if not args.prepare:
print str(verified)
return
2015-03-30 08:34:22 -04:00
verified.prepare()
available = verified.available()
logging.debug("Prepared plugins: %s", available)
print str(available)
2015-03-30 08:34:22 -04:00
def read_file(filename):
"""Returns the given file's contents with universal new line support.
:param str filename: Filename
:returns: A tuple of filename and its contents
:rtype: tuple
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
"""
try:
return filename, open(filename, "rU").read()
except IOError as exc:
raise argparse.ArgumentTypeError(exc.strerror)
2015-03-26 18:00:00 -04:00
def flag_default(name):
"""Default value for CLI flag."""
return constants.CLI_DEFAULTS[name]
2015-04-22 03:22:15 -04:00
def config_help(name):
"""Help message for `.IConfig` attribute."""
2015-04-22 03:22:15 -04:00
return interfaces.IConfig[name].__doc__
2015-03-26 18:00:00 -04:00
2015-05-02 04:53:06 -04:00
def create_parser(plugins):
2015-01-31 06:28:33 -05:00
"""Create parser."""
parser = configargparse.ArgParser(
description=__doc__,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
2015-04-22 03:09:23 -04:00
args_for_setting_config_path=["-c", "--config"],
default_config_files=flag_default("config_files"))
2015-04-22 03:22:15 -04:00
add = parser.add_argument
2015-03-30 08:34:22 -04:00
# --help is automatically provided by argparse
2015-04-22 03:22:15 -04:00
add("--version", action="version", version="%(prog)s {0}".format(
2015-05-04 04:26:08 -04:00
letsencrypt.__version__))
2015-04-22 03:22:15 -04:00
add("-v", "--verbose", dest="verbose_count", action="count",
default=flag_default("verbose_count"))
2015-04-22 03:22:15 -04:00
add("--no-confirm", dest="no_confirm", action="store_true",
help="Turn off confirmation screens, currently used for --revoke")
add("-e", "--agree-tos", dest="tos", action="store_true",
2015-04-22 03:22:15 -04:00
help="Skip the end user license agreement screen.")
add("-t", "--text", dest="text_mode", action="store_true",
2015-04-22 03:22:15 -04:00
help="Use the text output instead of the curses UI.")
2015-03-30 08:34:22 -04:00
subparsers = parser.add_subparsers(metavar="SUBCOMMAND")
2015-05-04 04:26:08 -04:00
def add_subparser(name, func): # pylint: disable=missing-docstring
2015-03-30 08:34:22 -04:00
subparser = subparsers.add_parser(
name, help=func.__doc__.splitlines()[0], description=func.__doc__)
subparser.set_defaults(func=func)
return subparser
2015-05-04 04:26:08 -04:00
add_subparser("run", run)
add_subparser("auth", auth)
add_subparser("install", install)
2015-03-30 08:34:22 -04:00
parser_revoke = add_subparser("revoke", revoke)
parser_rollback = add_subparser("rollback", rollback)
2015-05-04 04:26:08 -04:00
add_subparser("config_changes", config_changes)
2015-03-30 08:34:22 -04:00
2015-05-02 04:53:06 -04:00
parser_plugins = add_subparser("plugins", plugins_cmd)
2015-03-30 08:34:22 -04:00
parser_plugins.add_argument("--init", action="store_true")
parser_plugins.add_argument("--prepare", action="store_true")
parser_plugins.add_argument(
"--authenticators", action="append_const", dest="ifaces",
const=interfaces.IAuthenticator)
parser_plugins.add_argument(
"--installers", action="append_const", dest="ifaces",
const=interfaces.IInstaller)
2014-11-26 21:30:42 -05:00
2015-05-02 04:53:06 -04:00
parser.add_argument("--configurator")
parser.add_argument("-a", "--authenticator")
parser.add_argument("-i", "--installer")
2015-03-30 08:34:22 -04:00
# positional arg shadows --domains, instead of appending, and
# --domains is useful, because it can be stored in config
#for subparser in parser_run, parser_auth, parser_install:
# subparser.add_argument("domains", nargs="*", metavar="domain")
add("-d", "--domains", metavar="DOMAIN", action="append")
add("-s", "--server", default=flag_default("server"),
2015-02-09 17:39:49 -05:00
help=config_help("server"))
2015-02-16 02:17:53 -05:00
add("-k", "--authkey", type=read_file,
help="Path to the authorized key file")
add("-m", "--email", help=config_help("email"))
2015-04-02 02:55:50 -04:00
add("-B", "--rsa-key-size", type=int, metavar="N",
default=flag_default("rsa_key_size"), help=config_help("rsa_key_size"))
2015-04-22 03:22:15 -04:00
# TODO: resolve - assumes binary logic while client.py assumes ternary.
add("-r", "--redirect", action="store_true",
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
"authenticated vhost.")
2015-01-31 06:28:33 -05:00
2015-03-30 08:34:22 -04:00
parser_revoke.add_argument(
"--certificate", dest="rev_cert", type=read_file, metavar="CERT_PATH",
2015-02-16 02:17:53 -05:00
help="Revoke a specific certificate.")
2015-03-30 08:34:22 -04:00
parser_revoke.add_argument(
"--key", dest="rev_key", type=read_file, metavar="KEY_PATH",
2015-02-16 02:17:53 -05:00
help="Revoke all certs generated by the provided authorized key.")
2015-03-30 08:34:22 -04:00
parser_rollback.add_argument(
2015-04-02 02:55:50 -04:00
"--checkpoints", type=int, metavar="N",
default=flag_default("rollback_checkpoints"),
2015-01-31 06:28:33 -05:00
help="Revert configuration N number of checkpoints.")
2015-02-09 19:39:08 -05:00
2015-05-04 04:26:08 -04:00
_paths_parser(parser.add_argument_group("paths"))
# TODO: plugin_parser should be called for every detected plugin
2015-05-02 04:53:06 -04:00
for name, plugin_ep in plugins.iteritems():
plugin_ep.plugin_cls.inject_parser_options(
parser.add_argument_group(
name, description=plugin_ep.description), name)
2015-04-22 05:16:13 -04:00
2015-04-22 03:22:15 -04:00
return parser
2015-02-16 02:17:53 -05:00
2015-02-09 17:53:51 -05:00
2015-05-04 04:26:08 -04:00
def _paths_parser(parser):
2015-04-22 03:22:15 -04:00
add = parser.add_argument
add("--config-dir", default=flag_default("config_dir"),
help=config_help("config_dir"))
add("--work-dir", default=flag_default("work_dir"),
help=config_help("work_dir"))
add("--backup-dir", default=flag_default("backup_dir"),
help=config_help("backup_dir"))
add("--key-dir", default=flag_default("key_dir"),
help=config_help("key_dir"))
add("--cert-dir", default=flag_default("certs_dir"),
help=config_help("cert_dir"))
2015-01-31 06:28:33 -05:00
add("--le-vhost-ext", default="-le-ssl.conf",
help=config_help("le_vhost_ext"))
add("--cert-path", default=flag_default("cert_path"),
help=config_help("cert_path"))
add("--chain-path", default=flag_default("chain_path"),
help=config_help("chain_path"))
2015-01-31 06:28:33 -05:00
add("--enroll-autorenew", default=None, action="store_true",
help=config_help("enroll_autorenew"))
2015-04-22 03:22:15 -04:00
return parser
def main(args=sys.argv[1:]):
2015-01-31 06:28:33 -05:00
"""Command line argument parsing and main script execution."""
# note: arg parser internally handles --help (and exits afterwards)
2015-05-02 08:16:05 -04:00
plugins = plugins_disco.PluginsRegistry.find_all()
2015-05-02 04:53:06 -04:00
args = create_parser(plugins).parse_args(args)
2015-02-03 06:22:04 -05:00
config = configuration.NamespaceConfig(args)
2015-04-22 03:22:15 -04:00
# Displayer
if args.text_mode:
2015-04-22 03:22:15 -04:00
displayer = display_util.FileDisplay(sys.stdout)
else:
displayer = display_util.NcursesDisplay()
2015-04-22 03:22:15 -04:00
zope.component.provideUtility(displayer)
2015-04-22 03:22:15 -04:00
# Logging
2015-03-30 08:34:22 -04:00
level = -args.verbose_count * 10
logger = logging.getLogger()
2015-03-30 08:34:22 -04:00
logger.setLevel(level)
logging.debug("Logging level set at %d", level)
if not args.text_mode:
logger.addHandler(log.DialogHandler())
2015-04-22 03:22:15 -04:00
2015-05-04 08:46:24 -04:00
logging.debug("Discovered plugins: %r", plugins)
2015-04-22 03:22:15 -04:00
if not os.geteuid() == 0:
logging.warning(
"Root (sudo) is required to run most of letsencrypt functionality.")
# check must be done after arg parsing as --help should work
# w/o root; on the other hand, e.g. "letsencrypt run
# --authenticator dns" or "letsencrypt plugins" does not
# require root as well
#return (
# "{0}Root is required to run letsencrypt. Please use sudo.{0}"
# .format(os.linesep))
2015-05-02 04:53:06 -04:00
return args.func(args, config, plugins)
2014-11-26 21:30:42 -05:00
if __name__ == "__main__":
2015-04-22 03:20:45 -04:00
sys.exit(main()) # pragma: no cover