mirror of
https://github.com/certbot/certbot.git
synced 2026-06-03 22:08:07 -04:00
Merge branch 'master' into leauto-upgrades
This commit is contained in:
commit
b7c33ceeb8
28 changed files with 517 additions and 154 deletions
4
Vagrantfile
vendored
4
Vagrantfile
vendored
|
|
@ -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
2
acme/setup.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
5
setup.py
5
setup.py
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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%.*}
|
||||
|
|
|
|||
Loading…
Reference in a new issue