Merge branch 'master' into leauto-upgrades

This commit is contained in:
Peter Eckersley 2016-01-27 15:35:46 -08:00
commit b7c33ceeb8
28 changed files with 517 additions and 154 deletions

4
Vagrantfile vendored
View file

@ -21,6 +21,10 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Cannot allocate memory" when running
# letsencrypt.client.tests.display.util_test.NcursesDisplayTest
v.memory = 1024
# Handle cases when the host is behind a private network by making the
# NAT engine use the host's resolver mechanisms to handle DNS requests.
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end
end

2
acme/setup.cfg Normal file
View file

@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View file

@ -3,6 +3,7 @@
# Tested with:
# - Fedora 22, 23 (x64)
# - Centos 7 (x64: on DigitalOcean droplet)
# - CentOS 7 Minimal install in a Hyper-V VM
if type dnf 2>/dev/null
then
@ -21,12 +22,16 @@ fi
if ! $tool install -y \
python \
python-devel \
python-virtualenv
python-virtualenv \
python-tools \
python-pip
then
if ! $tool install -y \
python27 \
python27-devel \
python27-virtualenv
python27-virtualenv \
python27-tools \
python27-pip
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1

View file

@ -170,7 +170,7 @@ Changing your settings
This will probably look something like
..code-block: shell
.. code-block:: shell
letsencrypt --cipher-recommendations mozilla-secure
letsencrypt --cipher-recommendations mozilla-intermediate
@ -179,14 +179,14 @@ This will probably look something like
to track Mozilla's *Secure*, *Intermediate*, or *Old* recommendations,
and
..code-block: shell
.. code-block:: shell
letsencrypt --update-ciphers on
to enable updating ciphers with each new Let's Encrypt client release,
or
..code-block: shell
.. code-block:: shell
letsencrypt --update-ciphers off

View file

@ -96,11 +96,32 @@ Integration testing with the boulder CA
Generally it is sufficient to open a pull request and let Github and Travis run
integration tests for you.
Mac OS X users: Run `./tests/mac-bootstrap.sh` instead of `boulder-start.sh` to
install dependencies, configure the environment, and start boulder.
Mac OS X users: Run ``./tests/mac-bootstrap.sh`` instead of
``boulder-start.sh`` to install dependencies, configure the
environment, and start boulder.
Otherwise, install `Go`_ 1.5, libtool-ltdl, mariadb-server and
rabbitmq-server and then start Boulder_, an ACME CA server::
Otherwise, install `Go`_ 1.5, ``libtool-ltdl``, ``mariadb-server`` and
``rabbitmq-server`` and then start Boulder_, an ACME CA server.
If you can't get packages of Go 1.5 for your Linux system,
you can execute the following commands to install it:
.. code-block:: shell
wget https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz -P /tmp/
sudo tar -C /usr/local -xzf /tmp/go1.5.3.linux-amd64.tar.gz
if ! grep -Fxq "export GOROOT=/usr/local/go" ~/.profile ; then echo "export GOROOT=/usr/local/go" >> ~/.profile; fi
if ! grep -Fxq "export PATH=\\$GOROOT/bin:\\$PATH" ~/.profile ; then echo "export PATH=\\$GOROOT/bin:\\$PATH" >> ~/.profile; fi
These commands download `Go`_ 1.5.3 to ``/tmp/``, extracts to ``/usr/local``,
and then adds the export lines required to execute ``boulder-start.sh`` to
``~/.profile`` if they were not previously added
Make sure you execute the following command after `Go`_ finishes installing::
if ! grep -Fxq "export GOPATH=\\$HOME/go" ~/.profile ; then echo "export GOPATH=\\$HOME/go" >> ~/.profile; fi
Afterwards, you'd be able to start Boulder_ using the following command::
./tests/boulder-start.sh

View file

@ -155,7 +155,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Set Version
if self.version is None:
self.version = self.get_version()
if self.version < (2, 2):
if self.version < (2, 4):
raise errors.NotSupportedError(
"Apache Version %s not supported.", str(self.version))

View file

@ -4,6 +4,7 @@ import os
import zope.component
from letsencrypt import errors
from letsencrypt import interfaces
import letsencrypt.display.util as display_util
@ -78,12 +79,18 @@ def _vhost_menu(domain, vhosts):
name_size=disp_name_size)
)
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
"We were unable to find a vhost with a ServerName "
"or Address of {0}.{1}Which virtual host would you "
"like to choose?".format(
domain, os.linesep),
choices, help_label="More Info", ok_label="Select")
try:
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
"We were unable to find a vhost with a ServerName "
"or Address of {0}.{1}Which virtual host would you "
"like to choose?".format(domain, os.linesep),
choices, help_label="More Info", ok_label="Select")
except errors.MissingCommandlineFlag, e:
msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}"
"(The best solution is to add ServerName or ServerAlias "
"entries to the VirtualHost directives of your apache "
"configuration files.)".format(e, os.linesep))
raise errors.MissingCommandlineFlag, msg
return code, tag

View file

@ -6,6 +6,7 @@ import mock
import zope.component
from letsencrypt.display import util as display_util
from letsencrypt import errors
from letsencrypt_apache import obj
@ -31,6 +32,14 @@ class SelectVhostTest(unittest.TestCase):
mock_util().menu.return_value = (display_util.OK, 3)
self.assertEqual(self.vhosts[3], self._call(self.vhosts))
@mock.patch("letsencrypt_apache.display_ops.zope.component.getUtility")
def test_noninteractive(self, mock_util):
mock_util().menu.side_effect = errors.MissingCommandlineFlag("no vhost default")
try:
self._call(self.vhosts)
except errors.MissingCommandlineFlag, e:
self.assertTrue("VirtualHost directives" in e.message)
@mock.patch("letsencrypt_apache.display_ops.zope.component.getUtility")
def test_more_info_cancel(self, mock_util):
mock_util().menu.side_effect = [

View file

@ -76,6 +76,7 @@ class ApacheTlsSni01(common.TLSSNI01):
# Setup the configuration
addrs = self._mod_config()
self.configurator.save("Don't lose mod_config changes", True)
self.configurator.make_addrs_sni_ready(addrs)
# Save reversible changes

View file

@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
VENV_BIN=${VENV_PATH}/bin
LE_AUTO_VERSION="0.2.0.dev0"
LE_AUTO_VERSION="0.2.1.dev0"
# This script takes the same arguments as the main letsencrypt program, but it
# additionally responds to --verbose (more output) and --debug (allow support
@ -213,6 +213,7 @@ BootstrapRpmCommon() {
# Tested with:
# - Fedora 22, 23 (x64)
# - Centos 7 (x64: on DigitalOcean droplet)
# - CentOS 7 Minimal install in a Hyper-V VM
if type dnf 2>/dev/null
then
@ -231,12 +232,16 @@ BootstrapRpmCommon() {
if ! $SUDO $tool install -y \
python \
python-devel \
python-virtualenv
python-virtualenv \
python-tools \
python-pip
then
if ! $SUDO $tool install -y \
python27 \
python27-devel \
python27-virtualenv
python27-virtualenv \
python27-tools \
python27-pip
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1
@ -433,7 +438,7 @@ if [ "$NO_SELF_UPGRADE" = 1 ]; then
# -------------------------------------------------------------------------
cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt"
# This is the flattened list of packages letsencrypt-auto installs. To generate
# this, do `pip install -r -e acme -e . -e letsencrypt-apache`, `pip freeze`,
# this, do `pip install -e acme -e . -e letsencrypt-apache`, `pip freeze`,
# and then gather the hashes.
# sha256: wxZH7baf09RlqEfqMVfTe-0flfGXYLEaR6qRwEtmYxQ
@ -506,6 +511,9 @@ ipaddress==1.0.16
# sha256: 6MFV_evZxLywgQtO0BrhmHVUse4DTddTLXuP2uOKYnQ
ndg-httpsclient==0.4.0
# sha256: HDW0rCBs7y0kgWyJ-Jzyid09OM98RJuz-re_bUPwGx8
ordereddict==1.1
# sha256: OnTxAPkNZZGDFf5kkHca0gi8PxOv0y01_P5OjQs7gSs
# sha256: Paa-K-UG9ZzOMuGeMOIBBT4btNB-JWaJGOAPikmtQKs
parsedatetime==1.5
@ -745,7 +753,7 @@ except ImportError:
DownloadProgressBar = DownloadProgressSpinner = NullProgressBar
__version__ = 2, 5, 0
__version__ = 3, 0, 0
try:
from pip.index import FormatControl # noqa
@ -1003,9 +1011,11 @@ def package_finder(argv):
# Carry over PackageFinder kwargs that have [about] the same names as
# options attr names:
possible_options = [
'find_links', FORMAT_CONTROL_ARG, 'allow_external', 'allow_unverified',
'allow_all_external', ('allow_all_prereleases', 'pre'),
'process_dependency_links']
'find_links',
FORMAT_CONTROL_ARG,
('allow_all_prereleases', 'pre'),
'process_dependency_links'
]
kwargs = {}
for option in possible_options:
kw, attr = option if isinstance(option, tuple) else (option, option)

View file

@ -2,6 +2,7 @@ BootstrapRpmCommon() {
# Tested with:
# - Fedora 22, 23 (x64)
# - Centos 7 (x64: on DigitalOcean droplet)
# - CentOS 7 Minimal install in a Hyper-V VM
if type dnf 2>/dev/null
then
@ -20,12 +21,16 @@ BootstrapRpmCommon() {
if ! $SUDO $tool install -y \
python \
python-devel \
python-virtualenv
python-virtualenv \
python-tools \
python-pip
then
if ! $SUDO $tool install -y \
python27 \
python27-devel \
python27-virtualenv
python27-virtualenv \
python27-tools \
python27-pip
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1

View file

@ -1,5 +1,5 @@
# This is the flattened list of packages letsencrypt-auto installs. To generate
# this, do `pip install -r -e acme -e . -e letsencrypt-apache`, `pip freeze`,
# this, do `pip install -e acme -e . -e letsencrypt-apache`, `pip freeze`,
# and then gather the hashes.
# sha256: wxZH7baf09RlqEfqMVfTe-0flfGXYLEaR6qRwEtmYxQ
@ -72,6 +72,9 @@ ipaddress==1.0.16
# sha256: 6MFV_evZxLywgQtO0BrhmHVUse4DTddTLXuP2uOKYnQ
ndg-httpsclient==0.4.0
# sha256: HDW0rCBs7y0kgWyJ-Jzyid09OM98RJuz-re_bUPwGx8
ordereddict==1.1
# sha256: OnTxAPkNZZGDFf5kkHca0gi8PxOv0y01_P5OjQs7gSs
# sha256: Paa-K-UG9ZzOMuGeMOIBBT4btNB-JWaJGOAPikmtQKs
parsedatetime==1.5

View file

@ -104,7 +104,7 @@ except ImportError:
DownloadProgressBar = DownloadProgressSpinner = NullProgressBar
__version__ = 2, 5, 0
__version__ = 3, 0, 0
try:
from pip.index import FormatControl # noqa
@ -362,9 +362,11 @@ def package_finder(argv):
# Carry over PackageFinder kwargs that have [about] the same names as
# options attr names:
possible_options = [
'find_links', FORMAT_CONTROL_ARG, 'allow_external', 'allow_unverified',
'allow_all_external', ('allow_all_prereleases', 'pre'),
'process_dependency_links']
'find_links',
FORMAT_CONTROL_ARG,
('allow_all_prereleases', 'pre'),
'process_dependency_links'
]
kwargs = {}
for option in possible_options:
kw, attr = option if isinstance(option, tuple) else (option, option)

View file

@ -44,6 +44,15 @@ from letsencrypt.plugins import disco as plugins_disco
logger = logging.getLogger(__name__)
# For help strings, figure out how the user ran us.
# When invoked from letsencrypt-auto, sys.argv[0] is something like:
# "/home/user/.local/share/letsencrypt/bin/letsencrypt"
# Note that this won't work if the user set VENV_PATH or XDG_DATA_HOME before running
# letsencrypt-auto (and sudo stops us from seeing if they did), so it should only be used
# for purposes where inability to detect letsencrypt-auto fails safely
fragment = os.path.join(".local", "share", "letsencrypt")
cli_command = "letsencrypt-auto" if fragment in sys.argv[0] else "letsencrypt"
# Argparse's help formatting has a lot of unhelpful peculiarities, so we want
# to replace as much of it as we can...
@ -51,7 +60,7 @@ logger = logging.getLogger(__name__)
# This is the stub to include in help generated by argparse
SHORT_USAGE = """
letsencrypt [SUBCOMMAND] [options] [-d domain] [-d domain] ...
{0} [SUBCOMMAND] [options] [-d domain] [-d domain] ...
The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates. By
default, it will attempt to use a webserver both for obtaining and installing
@ -65,7 +74,7 @@ the cert. Major SUBCOMMANDS are:
config_changes Show changes made to server config during installation
plugins Display information about installed plugins
"""
""".format(cli_command)
# This is the short help for letsencrypt --help, where we disable argparse
# altogether
@ -155,12 +164,14 @@ def _determine_account(args, config):
"must agree in order to register with the ACME "
"server at {1}".format(
regr.terms_of_service, config.server))
return zope.component.getUtility(interfaces.IDisplay).yesno(
msg, "Agree", "Cancel")
obj = zope.component.getUtility(interfaces.IDisplay)
return obj.yesno(msg, "Agree", "Cancel", cli_flag="--agree-tos")
try:
acc, acme = client.register(
config, account_storage, tos_cb=_tos_cb)
except errors.MissingCommandlineFlag:
raise
except errors.Error as error:
logger.debug(error, exc_info=True)
raise errors.Error(
@ -194,6 +205,8 @@ def _find_duplicative_certs(config, domains):
le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
for renewal_file in os.listdir(configs_dir):
if not renewal_file.endswith(".conf"):
continue
try:
full_path = os.path.join(configs_dir, renewal_file)
candidate_lineage = storage.RenewableCert(full_path, cli_config)
@ -280,7 +293,7 @@ def _handle_identical_cert_request(config, cert):
"Cancel this operation and do nothing"]
display = zope.component.getUtility(interfaces.IDisplay)
response = display.menu(question, choices, "OK", "Cancel")
response = display.menu(question, choices, "OK", "Cancel", default=0)
if response[0] == "cancel" or response[1] == 2:
# TODO: Add notification related to command-line options for
# skipping the menu for this case.
@ -315,7 +328,8 @@ def _handle_subset_cert_request(config, domains, cert):
", ".join(domains),
br=os.linesep)
if config.expand or config.renew_by_default or zope.component.getUtility(
interfaces.IDisplay).yesno(question, "Expand", "Cancel"):
interfaces.IDisplay).yesno(question, "Expand", "Cancel",
cli_flag="--expand (or in some cases, --duplicate)"):
return "renew", cert
else:
reporter_util = zope.component.getUtility(interfaces.IReporter)
@ -382,7 +396,7 @@ def _auth_from_domains(le_client, config, domains):
if action == "reinstall":
# The lineage already exists; allow the caller to try installing
# it without getting a new certificate at all.
return lineage
return lineage, "reinstall"
elif action == "renew":
original_server = lineage.configuration["renewalparams"]["server"]
_avoid_invalidating_lineage(config, lineage, original_server)
@ -407,7 +421,7 @@ def _auth_from_domains(le_client, config, domains):
_report_new_cert(lineage.cert, lineage.fullchain)
return lineage
return lineage, action
def _avoid_invalidating_lineage(config, lineage, original_server):
"Do not renew a valid cert with one from a staging server!"
@ -431,21 +445,6 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
"a test certificate (domains: {0}). We will not do that "
"unless you use the --break-my-certs flag!".format(names))
def set_configurator(previously, now):
"""
Setting configurators multiple ways is okay, as long as they all agree
:param str previously: previously identified request for the installer/authenticator
:param str requested: the request currently being processed
"""
if now is None:
# we're not actually setting anything
return previously
if previously:
if previously != now:
msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}"
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
return now
def diagnose_configurator_problem(cfg_type, requested, plugins):
"""
@ -479,22 +478,28 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
raise errors.PluginSelectionError(msg)
def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable=too-many-branches
def set_configurator(previously, now):
"""
Figure out which configurator we're going to use
:raises error.PluginSelectionError if there was a problem
Setting configurators multiple ways is okay, as long as they all agree
:param str previously: previously identified request for the installer/authenticator
:param str requested: the request currently being processed
"""
if now is None:
# we're not actually setting anything
return previously
if previously:
if previously != now:
msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}"
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
return now
# Which plugins do we need?
need_inst = need_auth = (verb == "run")
if verb == "certonly":
need_auth = True
if verb == "install":
need_inst = True
if args.authenticator:
logger.warn("Specifying an authenticator doesn't make sense in install mode")
def cli_plugin_requests(args):
"""
Figure out which plugins the user requested with CLI and config options
# Which plugins did the user request?
:returns: (requested authenticator string or None, requested installer string or None)
:rtype: tuple
"""
req_inst = req_auth = args.configurator
req_inst = set_configurator(req_inst, args.installer)
req_auth = set_configurator(req_auth, args.authenticator)
@ -511,6 +516,40 @@ def choose_configurator_plugins(args, config, plugins, verb): # pylint: disable
if args.manual:
req_auth = set_configurator(req_auth, "manual")
logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst)
return req_auth, req_inst
noninstaller_plugins = ["webroot", "manual", "standalone"]
def choose_configurator_plugins(args, config, plugins, verb):
"""
Figure out which configurator we're going to use
:raises errors.PluginSelectionError if there was a problem
"""
req_auth, req_inst = cli_plugin_requests(args)
# Which plugins do we need?
if verb == "run":
need_inst = need_auth = True
if req_auth in noninstaller_plugins and not req_inst:
msg = ('With the {0} plugin, you probably want to use the "certonly" command, eg:{1}'
'{1} {2} certonly --{0}{1}{1}'
'(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins'
'{1} and "--help plugins" for more information.)'.format(
req_auth, os.linesep, cli_command))
raise errors.MissingCommandlineFlag, msg
else:
need_inst = need_auth = False
if verb == "certonly":
need_auth = True
if verb == "install":
need_inst = True
if args.authenticator:
logger.warn("Specifying an authenticator doesn't make sense in install mode")
# Try to meet the user's request and/or ask them to pick plugins
authenticator = installer = None
@ -556,7 +595,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(args, config, authenticator, installer)
lineage = _auth_from_domains(le_client, config, domains)
lineage, action = _auth_from_domains(le_client, config, domains)
le_client.deploy_certificate(
domains, lineage.privkey, lineage.cert,
@ -567,7 +606,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
if len(lineage.available_versions("cert")) == 1:
display_ops.success_installation(domains)
else:
display_ops.success_renewal(domains)
display_ops.success_renewal(domains, action)
_suggest_donate()
@ -605,6 +644,8 @@ def obtain_cert(args, config, plugins):
def install(args, config, plugins):
"""Install a previously obtained cert in a server."""
# XXX: Update for renewer/RenewableCert
# FIXME: be consistent about whether errors are raised or returned from
# this function ...
try:
installer, _ = choose_configurator_plugins(args, config,
@ -946,6 +987,12 @@ def prepare_and_parse_args(plugins, args):
helpful.add(
None, "-t", "--text", dest="text_mode", action="store_true",
help="Use the text output instead of the curses UI.")
helpful.add(
None, "-n", "--non-interactive", "--noninteractive",
dest="noninteractive_mode", action="store_true",
help="Run without ever asking for user input. This may require "
"additional command line flags; the client will try to explain "
"which ones are required if it finds one missing")
helpful.add(
None, "--register-unsafely-without-email", action="store_true",
help="Specifying this flag enables registering an account with no "
@ -1093,7 +1140,8 @@ def _create_subparsers(helpful):
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER"
" format; note that the .csr file *must* contain a Subject"
" Alternative Name field for each domain you want certified.")
" Alternative Name field for each domain you want certified."
" Currently --csr only works with the 'certonly' subcommand'")
helpful.add("rollback",
"--checkpoints", type=int, metavar="N",
default=flag_default("rollback_checkpoints"),
@ -1166,9 +1214,8 @@ def _plugins_parsing(helpful, plugins):
"plugins", description="Let's Encrypt client supports an "
"extensible plugins architecture. See '%(prog)s plugins' for a "
"list of all installed plugins and their names. You can force "
"a particular plugin by setting options provided below. Further "
"down this help message you will find plugin-specific options "
"(prefixed by --{plugin_name}).")
"a particular plugin by setting options provided below. Running "
"--help <plugin_name> will list flags specific to that plugin.")
helpful.add(
"plugins", "-a", "--authenticator", help="Authenticator plugin name.")
helpful.add(
@ -1380,7 +1427,9 @@ def main(cli_args=sys.argv[1:]):
sys.excepthook = functools.partial(_handle_exception, args=args)
# Displayer
if args.text_mode:
if args.noninteractive_mode:
displayer = display_util.NoninteractiveDisplay(sys.stdout)
elif args.text_mode:
displayer = display_util.FileDisplay(sys.stdout)
else:
displayer = display_util.NcursesDisplay()

View file

@ -48,7 +48,7 @@ def redirect_by_default():
code, selection = util(interfaces.IDisplay).menu(
"Please choose whether HTTPS access is required or optional.",
choices)
choices, default=0, cli_flag="--redirect / --no-redirect")
if code != display_util.OK:
return False

View file

@ -31,8 +31,8 @@ def choose_plugin(prepared, question):
for plugin_ep in prepared]
while True:
code, index = util(interfaces.IDisplay).menu(
question, opts, help_label="More Info")
disp = util(interfaces.IDisplay)
code, index = disp.menu(question, opts, help_label="More Info")
if code == display_util.OK:
plugin_ep = prepared[index]
@ -74,6 +74,16 @@ def pick_plugin(config, default, plugins, question, ifaces):
# throw more UX-friendly error if default not in plugins
filtered = plugins.filter(lambda p_ep: p_ep.name == default)
else:
if config.noninteractive_mode:
# it's really bad to auto-select the single available plugin in
# non-interactive mode, because an update could later add a second
# available plugin
raise errors.MissingCommandlineFlag, ("Missing command line flags. For non-interactive "
"execution, you will need to specify a plugin on the command line. Run with "
"'--help plugins' to see a list of options, and see "
" https://eff.org/letsencrypt-plugins for more detail on what the plugins "
"do and how to use them.")
filtered = plugins.visible().ifaces(ifaces)
filtered.init(config)
@ -143,7 +153,12 @@ def get_email(more=False, invalid=False):
msg += ('\n\nIf you really want to skip this, you can run the client with '
'--register-unsafely-without-email but make sure you backup your '
'account key from /etc/letsencrypt/accounts\n\n')
code, email = zope.component.getUtility(interfaces.IDisplay).input(msg)
try:
code, email = zope.component.getUtility(interfaces.IDisplay).input(msg)
except errors.MissingCommandlineFlag:
msg = ("You should register before running non-interactively, or provide --agree-tos"
" and --email <email_address> flags")
raise errors.MissingCommandlineFlag, msg
if code == display_util.OK:
if le_util.safe_email(email):
@ -197,7 +212,8 @@ def choose_names(installer):
"specify ServerNames in your config files in order to allow for "
"accurate installation of your certificate.{0}"
"If you do use the default vhost, you may specify the name "
"manually. Would you like to continue?{0}".format(os.linesep))
"manually. Would you like to continue?{0}".format(os.linesep),
default=True)
if manual:
return _choose_names_manually()
@ -242,7 +258,7 @@ def _filter_names(names):
"""
code, names = util(interfaces.IDisplay).checklist(
"Which names would you like to activate HTTPS for?",
tags=names)
tags=names, cli_flag="--domains")
return code, [str(s) for s in names]
@ -250,7 +266,8 @@ def _choose_names_manually():
"""Manually input names for those without an installer."""
code, input_ = util(interfaces.IDisplay).input(
"Please enter in your domain name(s) (comma and/or space separated) ")
"Please enter in your domain name(s) (comma and/or space separated) ",
cli_flag="--domains")
if code == display_util.OK:
invalid_domains = dict()
@ -309,22 +326,24 @@ def success_installation(domains):
pause=False)
def success_renewal(domains):
def success_renewal(domains, action):
"""Display a box confirming the renewal of an existing certificate.
.. todo:: This should be centered on the screen
:param list domains: domain names which were renewed
:param str action: can be "reinstall" or "renew"
"""
util(interfaces.IDisplay).notification(
"Your existing certificate has been successfully renewed, and the "
"Your existing certificate has been successfully {3}ed, and the "
"new certificate has been installed.{1}{1}"
"The new certificate covers the following domains: {0}{1}{1}"
"You should test your configuration at:{1}{2}".format(
_gen_https_names(domains),
os.linesep,
os.linesep.join(_gen_ssl_lab_urls(domains))),
os.linesep.join(_gen_ssl_lab_urls(domains)),
action),
height=(14 + len(domains)),
pause=False)

View file

@ -6,7 +6,7 @@ import dialog
import zope.interface
from letsencrypt import interfaces
from letsencrypt import errors
WIDTH = 72
HEIGHT = 20
@ -21,6 +21,20 @@ CANCEL = "cancel"
HELP = "help"
"""Display exit code when for when the user requests more help."""
def _wrap_lines(msg):
"""Format lines nicely to 80 chars.
:param str msg: Original message
:returns: Formatted message respecting newlines in message
:rtype: str
"""
lines = msg.splitlines()
fixed_l = []
for line in lines:
fixed_l.append(textwrap.fill(line, 80))
return os.linesep.join(fixed_l)
class NcursesDisplay(object):
"""Ncurses-based display."""
@ -49,8 +63,8 @@ class NcursesDisplay(object):
"""
self.dialog.msgbox(message, height, width=self.width)
def menu(self, message, choices,
ok_label="OK", cancel_label="Cancel", help_label=""):
def menu(self, message, choices, ok_label="OK", cancel_label="Cancel",
help_label="", **unused_kwargs):
"""Display a menu.
:param str message: title of menu
@ -61,10 +75,11 @@ class NcursesDisplay(object):
:param str ok_label: label of the OK button
:param str help_label: label of the help button
:param dict unused_kwargs: absorbs default / cli_args
:returns: tuple of the form (`code`, `tag`) where
`code` - `str` display_util exit code
`tag` - `int` index corresponding to the item chosen
:returns: tuple of the form (`code`, `index`) where
`code` - int display exit code
`int` - index of the selected item
:rtype: tuple
"""
@ -97,20 +112,21 @@ class NcursesDisplay(object):
(str(i), choice) for i, choice in enumerate(choices, 1)
]
# pylint: disable=star-args
code, tag = self.dialog.menu(message, **menu_options)
code, index = self.dialog.menu(message, **menu_options)
if code == CANCEL:
return code, -1
return code, int(tag) - 1
return code, int(index) - 1
def input(self, message):
def input(self, message, **unused_kwargs):
"""Display an input box to the user.
:param str message: Message to display that asks for input.
:param dict _kwargs: absorbs default / cli_args
:returns: tuple of the form (code, string) where
:returns: tuple of the form (`code`, `string`) where
`code` - int display exit code
`string` - input entered by the user
@ -122,7 +138,7 @@ class NcursesDisplay(object):
return self.dialog.inputbox(message, width=self.width, height=height)
def yesno(self, message, yes_label="Yes", no_label="No"):
def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
"""Display a Yes/No dialog box.
Yes and No label must begin with different letters.
@ -130,6 +146,7 @@ class NcursesDisplay(object):
:param str message: message to display to user
:param str yes_label: label on the "yes" button
:param str no_label: label on the "no" button
:param dict _kwargs: absorbs default / cli_args
:returns: if yes_label was selected
:rtype: bool
@ -139,16 +156,17 @@ class NcursesDisplay(object):
message, self.height, self.width,
yes_label=yes_label, no_label=no_label)
def checklist(self, message, tags, default_status=True):
def checklist(self, message, tags, default_status=True, **unused_kwargs):
"""Displays a checklist.
:param message: Message to display before choices
:param list tags: where each is of type :class:`str` len(tags) > 0
:param bool default_status: If True, items are in a selected state by
default.
:param dict _kwargs: absorbs default / cli_args
:returns: tuple of the form (code, list_tags) where
:returns: tuple of the form (`code`, `list_tags`) where
`code` - int display exit code
`list_tags` - list of str tags selected by the user
@ -178,15 +196,15 @@ class FileDisplay(object):
"""
side_frame = "-" * 79
message = self._wrap_lines(message)
message = _wrap_lines(message)
self.outfile.write(
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
line=os.linesep, frame=side_frame, msg=message))
if pause:
raw_input("Press Enter to Continue")
def menu(self, message, choices,
ok_label="", cancel_label="", help_label=""):
def menu(self, message, choices, ok_label="", cancel_label="",
help_label="", **unused_kwargs):
# pylint: disable=unused-argument
"""Display a menu.
@ -197,10 +215,12 @@ class FileDisplay(object):
:param choices: Menu lines, len must be > 0
:type choices: list of tuples (tag, item) or
list of descriptions (tags will be enumerated)
:param dict _kwargs: absorbs default / cli_args
:returns: tuple of (`code`, `index`) where
`code` - str display exit code
`index` - int index of the user's selection
:returns: tuple of the form (code, tag) where
code - int display exit code
tag - str corresponding to the item chosen
:rtype: tuple
"""
@ -210,11 +230,12 @@ class FileDisplay(object):
return code, selection - 1
def input(self, message):
def input(self, message, **unused_kwargs):
# pylint: disable=no-self-use
"""Accept input from the user.
:param str message: message to display to the user
:param dict _kwargs: absorbs default / cli_args
:returns: tuple of (`code`, `input`) where
`code` - str display exit code
@ -230,7 +251,7 @@ class FileDisplay(object):
else:
return OK, ans
def yesno(self, message, yes_label="Yes", no_label="No"):
def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs):
"""Query the user with a yes/no question.
Yes and No label must begin with different letters, and must contain at
@ -239,6 +260,7 @@ class FileDisplay(object):
:param str message: question for the user
:param str yes_label: Label of the "Yes" parameter
:param str no_label: Label of the "No" parameter
:param dict _kwargs: absorbs default / cli_args
:returns: True for "Yes", False for "No"
:rtype: bool
@ -246,7 +268,7 @@ class FileDisplay(object):
"""
side_frame = ("-" * 79) + os.linesep
message = self._wrap_lines(message)
message = _wrap_lines(message)
self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
os.linesep, frame=side_frame, msg=message))
@ -265,13 +287,14 @@ class FileDisplay(object):
ans.startswith(no_label[0].upper())):
return False
def checklist(self, message, tags, default_status=True):
def checklist(self, message, tags, default_status=True, **unused_kwargs):
# pylint: disable=unused-argument
"""Display a checklist.
:param str message: Message to display to user
:param list tags: `str` tags to select, len(tags) > 0
:param bool default_status: Not used for FileDisplay
:param dict _kwargs: absorbs default / cli_args
:returns: tuple of (`code`, `tags`) where
`code` - str display exit code
@ -352,21 +375,6 @@ class FileDisplay(object):
self.outfile.write(side_frame)
def _wrap_lines(self, msg): # pylint: disable=no-self-use
"""Format lines nicely to 80 chars.
:param str msg: Original message
:returns: Formatted message respecting newlines in message
:rtype: str
"""
lines = msg.splitlines()
fixed_l = []
for line in lines:
fixed_l.append(textwrap.fill(line, 80))
return os.linesep.join(fixed_l)
def _get_valid_int_ans(self, max_):
"""Get a numerical selection.
@ -403,6 +411,118 @@ class FileDisplay(object):
return OK, selection
class NoninteractiveDisplay(object):
"""An iDisplay implementation that never asks for interactive user input"""
zope.interface.implements(interfaces.IDisplay)
def __init__(self, outfile):
super(NoninteractiveDisplay, self).__init__()
self.outfile = outfile
def _interaction_fail(self, message, cli_flag, extra=""):
"Error out in case of an attempt to interact in noninteractive mode"
msg = "Missing command line flag or config entry for this setting:\n"
msg += message
if extra:
msg += "\n" + extra
if cli_flag:
msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
raise errors.MissingCommandlineFlag, msg
def notification(self, message, height=10, pause=False):
# pylint: disable=unused-argument
"""Displays a notification without waiting for user acceptance.
:param str message: Message to display to stdout
:param int height: No effect for NoninteractiveDisplay
:param bool pause: The NoninteractiveDisplay waits for no keyboard
"""
side_frame = "-" * 79
message = _wrap_lines(message)
self.outfile.write(
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
line=os.linesep, frame=side_frame, msg=message))
def menu(self, message, choices, ok_label=None, cancel_label=None,
default=None, cli_flag=None):
# pylint: disable=unused-argument,too-many-arguments
"""Avoid displaying a menu.
:param str message: title of menu
:param choices: Menu lines, len must be > 0
:type choices: list of tuples (tag, item) or
list of descriptions (tags will be enumerated)
:param int default: the default choice
:param dict kwargs: absorbs various irrelevant labelling arguments
:returns: tuple of (`code`, `index`) where
`code` - str display exit code
`index` - int index of the user's selection
:rtype: tuple
:raises errors.MissingCommandlineFlag: if there was no default
"""
if default is None:
self._interaction_fail(message, cli_flag, "Choices: " + repr(choices))
return OK, default
def input(self, message, default=None, cli_flag=None):
"""Accept input from the user.
:param str message: message to display to the user
:returns: tuple of (`code`, `input`) where
`code` - str display exit code
`input` - str of the user's input
:rtype: tuple
:raises errors.MissingCommandlineFlag: if there was no default
"""
if default is None:
self._interaction_fail(message, cli_flag)
else:
return OK, default
def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None):
# pylint: disable=unused-argument
"""Decide Yes or No, without asking anybody
:param str message: question for the user
:param dict kwargs: absorbs yes_label, no_label
:raises errors.MissingCommandlineFlag: if there was no default
:returns: True for "Yes", False for "No"
:rtype: bool
"""
if default is None:
self._interaction_fail(message, cli_flag)
else:
return default
def checklist(self, message, tags, default=None, cli_flag=None, **kwargs):
# pylint: disable=unused-argument
"""Display a checklist.
:param str message: Message to display to user
:param list tags: `str` tags to select, len(tags) > 0
:param dict kwargs: absorbs default_status arg
:returns: tuple of (`code`, `tags`) where
`code` - str display exit code
`tags` - list of selected tags
:rtype: tuple
"""
if default is None:
self._interaction_fail(message, cli_flag, "? ".join(tags))
else:
return OK, default
def separate_list_input(input_):
"""Separate a comma or space separated list.

View file

@ -102,3 +102,8 @@ class StandaloneBindError(Error):
class ConfigurationError(Error):
"""Configuration sanity error."""
# NoninteractiveDisplay iDisplay plugin error:
class MissingCommandlineFlag(Error):
"""A command line argument was missing in noninteractive usage"""

View file

@ -365,8 +365,8 @@ class IDisplay(zope.interface.Interface):
"""
def menu(message, choices,
ok_label="OK", cancel_label="Cancel", help_label=""):
def menu(message, choices, ok_label="OK", # pylint: disable=too-many-arguments
cancel_label="Cancel", help_label="", default=None, cli_flag=None):
"""Displays a generic menu.
:param str message: message to display
@ -377,14 +377,19 @@ class IDisplay(zope.interface.Interface):
:param str ok_label: label for OK button
:param str cancel_label: label for Cancel button
:param str help_label: label for Help button
:param int default: default (non-interactive) choice from the menu
:param str cli_flag: to automate choice from the menu, eg "--keep"
:returns: tuple of (`code`, `index`) where
`code` - str display exit code
`index` - int index of the user's selection
:raises errors.MissingCommandlineFlag: if called in non-interactive
mode without a default set
"""
def input(message):
def input(message, default=None, cli_args=None):
"""Accept input from the user.
:param str message: message to display to the user
@ -394,27 +399,45 @@ class IDisplay(zope.interface.Interface):
`input` - str of the user's input
:rtype: tuple
:raises errors.MissingCommandlineFlag: if called in non-interactive
mode without a default set
"""
def yesno(message, yes_label="Yes", no_label="No"):
def yesno(message, yes_label="Yes", no_label="No", default=None,
cli_args=None):
"""Query the user with a yes/no question.
Yes and No label must begin with different letters.
:param str message: question for the user
:param str default: default (non-interactive) choice from the menu
:param str cli_flag: to automate choice from the menu, eg "--redirect / --no-redirect"
:returns: True for "Yes", False for "No"
:rtype: bool
:raises errors.MissingCommandlineFlag: if called in non-interactive
mode without a default set
"""
def checklist(message, tags, default_state):
def checklist(message, tags, default_state, default=None, cli_args=None):
"""Allow for multiple selections from a menu.
:param str message: message to display to the user
:param list tags: where each is of type :class:`str` len(tags) > 0
:param bool default_status: If True, items are in a selected state by
default.
:param bool default_status: If True, items are in a selected state by default.
:param str default: default (non-interactive) state of the checklist
:param str cli_flag: to automate choice from the menu, eg "--domains"
:returns: tuple of the form (code, list_tags) where
`code` - int display exit code
`list_tags` - list of str tags selected by the user
:rtype: tuple
:raises errors.MissingCommandlineFlag: if called in non-interactive
mode without a default set
"""

View file

@ -165,7 +165,8 @@ s.serve_forever()" """
else:
if not self.conf("public-ip-logging-ok"):
if not zope.component.getUtility(interfaces.IDisplay).yesno(
self.IP_DISCLAIMER, "Yes", "No"):
self.IP_DISCLAIMER, "Yes", "No",
cli_flag="--manual-public-ip-logging-ok"):
raise errors.PluginError("Must agree to IP logging to proceed")
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(

View file

@ -172,6 +172,8 @@ def main(cli_args=sys.argv[1:]):
constants.CONFIG_DIRS_MODE, uid)
for renewal_file in os.listdir(cli_config.renewal_configs_dir):
if not renewal_file.endswith(".conf"):
continue
print("Processing " + renewal_file)
try:
# TODO: Before trying to initialize the RenewableCert object,

View file

@ -81,7 +81,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(1, mock_run.call_count)
def _help_output(self, args):
"Run a help command, and return the help string for scrutiny"
"Run a command, and return the ouput string for scrutiny"
output = StringIO.StringIO()
with mock.patch('letsencrypt.cli.sys.stdout', new=output):
self.assertRaises(SystemExit, self._call_stdout, args)
@ -105,6 +105,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertTrue("--checkpoints" not in out)
out = self._help_output(['-h'])
self.assertTrue("letsencrypt-auto" not in out) # test cli.cli_command
if "nginx" in plugins:
self.assertTrue("Use the Nginx plugin" in out)
else:
@ -130,16 +131,39 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
out = self._help_output(['-h'])
self.assertTrue(cli.usage_strings(plugins)[0] in out)
def _cli_missing_flag(self, args, message):
"Ensure that a particular error raises a missing cli flag error containing message"
exc = None
try:
with mock.patch('letsencrypt.cli.sys.stderr'):
cli.main(self.standard_args + args[:]) # NOTE: parser can alter its args!
except errors.MissingCommandlineFlag, exc:
self.assertTrue(message in str(exc))
self.assertTrue(exc is not None)
def test_noninteractive(self):
args = ['-n', 'certonly']
self._cli_missing_flag(args, "specify a plugin")
args.extend(['--standalone', '-d', 'eg.is'])
self._cli_missing_flag(args, "register before running")
with mock.patch('letsencrypt.cli._auth_from_domains'):
with mock.patch('letsencrypt.cli.client.acme_from_config_key'):
args.extend(['--email', 'io@io.is'])
self._cli_missing_flag(args, "--agree-tos")
@mock.patch('letsencrypt.cli.client.acme_client.Client')
@mock.patch('letsencrypt.cli._determine_account')
@mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate')
@mock.patch('letsencrypt.cli._auth_from_domains')
def test_user_agent(self, _afd, _obt, det, _client):
def test_user_agent(self, afd, _obt, det, _client):
# Normally the client is totally mocked out, but here we need more
# arguments to automate it...
args = ["--standalone", "certonly", "-m", "none@none.com",
"-d", "example.com", '--agree-tos'] + self.standard_args
det.return_value = mock.MagicMock(), None
afd.return_value = mock.MagicMock(), "newcert"
with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net:
self._call_no_clientmock(args)
os_ver = " ".join(le_util.get_os_info())
@ -208,6 +232,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
ret, _, _, _ = self._call(args)
self.assertTrue("--webroot-path must be set" in ret)
self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably")
with mock.patch("letsencrypt.cli._init_le_client") as mock_init:
with mock.patch("letsencrypt.cli._auth_from_domains"):
self._call(["certonly", "--manual", "-d", "foo.bar"])

View file

@ -69,7 +69,7 @@ class PickPluginTest(unittest.TestCase):
"""Tests for letsencrypt.display.ops.pick_plugin."""
def setUp(self):
self.config = mock.Mock()
self.config = mock.Mock(noninteractive_mode=False)
self.default = None
self.reg = mock.MagicMock()
self.question = "Question?"
@ -465,7 +465,7 @@ class SuccessRenewalTest(unittest.TestCase):
@classmethod
def _call(cls, names):
from letsencrypt.display.ops import success_renewal
success_renewal(names)
success_renewal(names, "renew")
@mock.patch("letsencrypt.display.ops.util")
def test_success_renewal(self, mock_util):

View file

@ -4,6 +4,8 @@ import unittest
import mock
import letsencrypt.errors as errors
from letsencrypt.display import util as display_util
@ -250,7 +252,7 @@ class FileOutputDisplayTest(unittest.TestCase):
"This function is only meant to be for easy viewing{0}"
"Test a really really really really really really really really "
"really really really really long line...".format(os.linesep))
text = self.displayer._wrap_lines(msg)
text = display_util._wrap_lines(msg)
self.assertEqual(text.count(os.linesep), 3)
@ -278,6 +280,46 @@ class FileOutputDisplayTest(unittest.TestCase):
self.displayer._get_valid_int_ans(3),
(display_util.CANCEL, -1))
class NoninteractiveDisplayTest(unittest.TestCase):
"""Test non-interactive display.
These tests are pretty easy!
"""
def setUp(self):
super(NoninteractiveDisplayTest, self).setUp()
self.mock_stdout = mock.MagicMock()
self.displayer = display_util.NoninteractiveDisplay(self.mock_stdout)
def test_notification_no_pause(self):
self.displayer.notification("message", 10)
string = self.mock_stdout.write.call_args[0][0]
self.assertTrue("message" in string)
def test_input(self):
d = "an incomputable value"
ret = self.displayer.input("message", default=d)
self.assertEqual(ret, (display_util.OK, d))
self.assertRaises(errors.MissingCommandlineFlag, self.displayer.input, "message")
def test_menu(self):
ret = self.displayer.menu("message", CHOICES, default=1)
self.assertEqual(ret, (display_util.OK, 1))
self.assertRaises(errors.MissingCommandlineFlag, self.displayer.menu, "message", CHOICES)
def test_yesno(self):
d = False
ret = self.displayer.yesno("message", default=d)
self.assertEqual(ret, d)
self.assertRaises(errors.MissingCommandlineFlag, self.displayer.yesno, "message")
def test_checklist(self):
d = [1, 3]
ret = self.displayer.checklist("message", TAGS, default=d)
self.assertEqual(ret, (display_util.OK, d))
self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS)
class SeparateListInputTest(unittest.TestCase):
"""Test Module functions."""

View file

@ -68,6 +68,13 @@ class BaseRenewableCertTest(unittest.TestCase):
config.write()
self.config = config
# We also create a file that isn't a renewal config in the same
# location to test that logic that reads in all-and-only renewal
# configs will ignore it and NOT attempt to parse it.
junk = open(os.path.join(self.tempdir, "renewal", "IGNORE.THIS"), "w")
junk.write("This file should be ignored!")
junk.close()
self.defaults = configobj.ConfigObj()
self.test_rc = storage.RenewableCert(config.filename, self.cli_config)

View file

@ -33,7 +33,10 @@ version = meta['version']
# Please update tox.ini when modifying dependency version requirements
install_requires = [
'acme=={0}'.format(version),
'ConfigArgParse>=0.10.0', # python2.6 support, upstream #17
# We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but
# saying so here causes a runtime error against our temporary fork of 0.9.3
# in which we added 2.6 support (see #2243), so we relax the requirement.
'ConfigArgParse>=0.9.3',
'configobj',
'cryptography>=0.7', # load_pem_x509_certificate
'parsedatetime',

View file

@ -29,7 +29,7 @@ unset PIP_INDEX_URL
export PIP_EXTRA_INDEX_URL="$SAVE"
git checkout -f "$BRANCH"
if ! ./letsencrypt-auto -v --debug --version | grep 0.1.1 ; then
if ! ./letsencrypt-auto -v --debug --version | grep 0.3.0 ; then
echo upgrade appeared to fail
exit 1
fi

View file

@ -81,21 +81,6 @@ if [ "$RELEASE_BRANCH" != "candidate-$version" ] ; then
fi
git checkout "$RELEASE_BRANCH"
# ensure we have the latest built version of leauto
letsencrypt-auto-source/build.py
# and that it's signed correctly
if ! openssl dgst -sha256 -verify $RELEASE_OPENSSL_PUBKEY -signature \
letsencrypt-auto-source/letsencrypt-auto.sig \
letsencrypt-auto-source/letsencrypt-auto ; then
echo Failed letsencrypt-auto signature check on "$RELEASE_BRANCH"
echo please fix that and re-run
exit 1
else
echo Signature check on letsencrypt-auto successful
fi
SetVersion() {
ver="$1"
for pkg_dir in $SUBPKGS letsencrypt-compatibility-test
@ -110,9 +95,6 @@ SetVersion() {
}
SetVersion "$version"
git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version"
git tag --local-user "$RELEASE_GPG_KEY" \
--sign --message "Release $version" "$tag"
echo "Preparing sdists and wheels"
for pkg_dir in . $SUBPKGS
@ -175,6 +157,21 @@ for module in letsencrypt $subpkgs_modules ; do
done
deactivate
# ensure we have the latest built version of leauto
letsencrypt-auto-source/build.py
# and that it's signed correctly
while ! openssl dgst -sha256 -verify $RELEASE_OPENSSL_PUBKEY -signature \
letsencrypt-auto-source/letsencrypt-auto.sig \
letsencrypt-auto-source/letsencrypt-auto ; do
read -p "Please correctly sign letsencrypt-auto with offline-signrequest.sh"
done
git diff --cached
git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version"
git tag --local-user "$RELEASE_GPG_KEY" \
--sign --message "Release $version" "$tag"
cd ..
echo Now in $PWD
name=${root_without_le%.*}