mirror of
https://github.com/certbot/certbot.git
synced 2026-06-06 23:32:06 -04:00
Fixed print, fixed logging, made display work
This commit is contained in:
parent
2db2060f85
commit
243cc4f9fb
3 changed files with 240 additions and 59 deletions
|
|
@ -1,4 +1,5 @@
|
|||
"""Lets Encrypt display."""
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
import dialog
|
||||
|
|
@ -14,7 +15,13 @@ HEIGHT = 20
|
|||
class CommonDisplayMixin(object): # pylint: disable=too-few-public-methods
|
||||
"""Mixin with methods common to classes implementing IDisplay."""
|
||||
|
||||
def redirect_by_default(self): # pylint: disable=missing-docstring
|
||||
def redirect_by_default(self):
|
||||
"""Determines whether the user would like to redirect to HTTPS.
|
||||
|
||||
:returns: True if redirect is desired, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
choices = [
|
||||
("Easy", "Allow both HTTP and HTTPS access to these sites"),
|
||||
("Secure", "Make all requests redirect to secure HTTPS access")]
|
||||
|
|
@ -41,12 +48,29 @@ class NcursesDisplay(CommonDisplayMixin):
|
|||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def generic_notification(self, message):
|
||||
# pylint: disable=missing-docstring
|
||||
self.dialog.msgbox(message, width=self.width)
|
||||
def generic_notification(self, message, height=10):
|
||||
"""Display a notification to the user and wait for user acceptance.
|
||||
|
||||
:param str message: Message to display
|
||||
:param int height: Height of the dialog box
|
||||
|
||||
"""
|
||||
self.dialog.msgbox(message, height, width=self.width)
|
||||
|
||||
def generic_menu(self, message, choices, unused_input_text=""):
|
||||
# pylint: disable=missing-docstring
|
||||
"""Display a menu.
|
||||
|
||||
:param str message: title of menu
|
||||
:param choices: Menu lines
|
||||
:type choices: list of tuples (tag, item) or
|
||||
list of items (tags will be enumerated)
|
||||
|
||||
:returns: tuple of the form (code, tag) where
|
||||
code is a display exit code
|
||||
tag is the tag string corresponding to the item chosen
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
# Can accept either tuples or just the actual choices
|
||||
if choices and isinstance(choices[0], tuple):
|
||||
code, selection = self.dialog.menu(
|
||||
|
|
@ -57,18 +81,46 @@ class NcursesDisplay(CommonDisplayMixin):
|
|||
code, tag = self.dialog.menu(
|
||||
message, choices=choices, width=self.width, height=self.height)
|
||||
|
||||
return code(int(tag) - 1)
|
||||
return code, int(tag) - 1
|
||||
|
||||
def generic_input(self, message): # pylint: disable=missing-docstring
|
||||
def generic_input(self, message):
|
||||
"""Display an input box to the user.
|
||||
|
||||
:param str message: Message to display that asks for input.
|
||||
|
||||
:returns: tuple of the form (code, string) where
|
||||
code is a display exit code
|
||||
string is the input entered by the user
|
||||
|
||||
"""
|
||||
return self.dialog.inputbox(message)
|
||||
|
||||
def generic_yesno(self, message, yes_label="Yes", no_label="No"):
|
||||
# pylint: disable=missing-docstring
|
||||
"""Display a Yes/No dialog box
|
||||
|
||||
: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
|
||||
|
||||
:returns: if yes_label was selected
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return self.dialog.DIALOG_OK == self.dialog.yesno(
|
||||
message, self.height, self.width,
|
||||
yes_label=yes_label, no_label=no_label)
|
||||
|
||||
def filter_names(self, names): # pylint: disable=missing-docstring
|
||||
def filter_names(self, names):
|
||||
"""Determine which names the user would like to select from a list.
|
||||
|
||||
:param list names: domain names
|
||||
|
||||
:returns: tuple of the form (code, names) where
|
||||
code is a display exit code
|
||||
names is a list of names selected
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
choices = [(n, "", 0) for n in names]
|
||||
code, names = self.dialog.checklist(
|
||||
"Which names would you like to activate HTTPS for?",
|
||||
|
|
@ -76,12 +128,26 @@ class NcursesDisplay(CommonDisplayMixin):
|
|||
return code, [str(s) for s in names]
|
||||
|
||||
def success_installation(self, domains):
|
||||
# pylint: disable=missing-docstring
|
||||
"""Display a box confirming the installation of HTTPS.
|
||||
|
||||
:param list domains: domain names which were enabled
|
||||
|
||||
"""
|
||||
self.dialog.msgbox(
|
||||
"\nCongratulations! You have successfully enabled "
|
||||
+ gen_https_names(domains) + "!", width=self.width)
|
||||
|
||||
def display_certs(self, certs): # pylint: disable=missing-docstring
|
||||
def display_certs(self, certs):
|
||||
"""Display certificates for revocation.
|
||||
|
||||
:param list certs: `list` of `dict` used throughout revoker.py
|
||||
|
||||
:returns: tuple of the form (code, selection) where
|
||||
code is a display exit code
|
||||
selection is the user's int selection
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
list_choices = [
|
||||
(str(i+1), "%s | %s | %s" %
|
||||
(str(c["cn"].ljust(self.width - 39)),
|
||||
|
|
@ -98,7 +164,15 @@ class NcursesDisplay(CommonDisplayMixin):
|
|||
tag = -1
|
||||
return code, (int(tag) - 1)
|
||||
|
||||
def confirm_revocation(self, cert): # pylint: disable=missing-docstring
|
||||
def confirm_revocation(self, cert):
|
||||
"""Confirm revocation screen.
|
||||
|
||||
:param dict cert: cert dict used throughout revoker.py
|
||||
|
||||
:returns: True if user would like to revoke, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
text = ("Are you sure you would like to revoke the following "
|
||||
"certificate:\n")
|
||||
text += cert_info_frame(cert)
|
||||
|
|
@ -106,10 +180,14 @@ class NcursesDisplay(CommonDisplayMixin):
|
|||
return self.dialog.DIALOG_OK == self.dialog.yesno(
|
||||
text, width=self.width, height=self.height)
|
||||
|
||||
def more_info_cert(self, cert): # pylint: disable=missing-docstring
|
||||
def more_info_cert(self, cert):
|
||||
"""Displays more information about the certificate.
|
||||
|
||||
:param dict cert: cert dict used throughout revoker.py
|
||||
|
||||
"""
|
||||
text = "Certificate Information:\n"
|
||||
text += cert_info_frame(cert)
|
||||
print text
|
||||
self.dialog.msgbox(text, width=self.width, height=self.height)
|
||||
|
||||
|
||||
|
|
@ -122,15 +200,36 @@ class FileDisplay(CommonDisplayMixin):
|
|||
super(FileDisplay, self).__init__()
|
||||
self.outfile = outfile
|
||||
|
||||
def generic_notification(self, message):
|
||||
# pylint: disable=missing-docstring
|
||||
def generic_notification(self, message, unused_height):
|
||||
"""Displays a notification and waits for user acceptance.
|
||||
|
||||
:param str message: Message to display
|
||||
|
||||
"""
|
||||
side_frame = '-' * 79
|
||||
msg = textwrap.fill(message, 80)
|
||||
self.outfile.write("\n%s\n%s\n%s\n" % (side_frame, msg, side_frame))
|
||||
lines = message.splitlines()
|
||||
fixed_l = []
|
||||
for line in lines:
|
||||
fixed_l.append(textwrap.fill(line, 80))
|
||||
self.outfile.write(
|
||||
"{0}{1}{0}{2}{0}{1}{0}".format(
|
||||
os.linesep, side_frame, os.linesep.join(fixed_l)))
|
||||
raw_input("Press Enter to Continue")
|
||||
|
||||
def generic_menu(self, message, choices, input_text=""):
|
||||
# pylint: disable=missing-docstring
|
||||
"""Display a menu.
|
||||
|
||||
:param str message: title of menu
|
||||
:param choices: Menu lines
|
||||
:type choices: list of tuples (tag, item) or
|
||||
list of items (tags will be enumerated)
|
||||
|
||||
:returns: tuple of the form (code, tag) where
|
||||
code is a display exit code
|
||||
tag is the tag string corresponding to the item chosen
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
# Can take either tuples or single items in choices list
|
||||
if choices and isinstance(choices[0], tuple):
|
||||
choices = ["%s - %s" % (c[0], c[1]) for c in choices]
|
||||
|
|
@ -145,27 +244,54 @@ class FileDisplay(CommonDisplayMixin):
|
|||
|
||||
self.outfile.write("%s\n" % side_frame)
|
||||
|
||||
code, selection = self.__get_valid_int_ans(
|
||||
code, selection = self._get_valid_int_ans(
|
||||
"%s (c to cancel): " % input_text)
|
||||
|
||||
return code, (selection - 1)
|
||||
|
||||
def generic_input(self, message):
|
||||
# pylint: disable=no-self-use,missing-docstring
|
||||
# pylint: disable=no-self-use
|
||||
"""Accept input from the user
|
||||
|
||||
:param str message: message to display to the user
|
||||
|
||||
:returns: tuple of (code, input) where
|
||||
code is a display exit code
|
||||
input is a str of the user's input
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
ans = raw_input("%s (Enter c to cancel)\n" % message)
|
||||
|
||||
if ans.startswith('c') or ans.startswith('C'):
|
||||
return CANCEL, -1
|
||||
if ans == 'c' or ans == 'C':
|
||||
return CANCEL, "-1"
|
||||
else:
|
||||
return OK, ans
|
||||
|
||||
def generic_yesno(self, message, unused_yes_label="", unused_no_label=""):
|
||||
# pylint: disable=missing-docstring
|
||||
"""Query the user with a yes/no question.
|
||||
|
||||
:param str message: question for the user
|
||||
|
||||
:returns: True for 'Yes', False for 'No"
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
self.outfile.write("\n%s\n" % textwrap.fill(message, 80))
|
||||
ans = raw_input("y/n: ")
|
||||
return ans.startswith('y') or ans.startswith('Y')
|
||||
|
||||
def filter_names(self, names): # pylint: disable=missing-docstring
|
||||
def filter_names(self, names):
|
||||
"""Determine which names the user would like to select from a list.
|
||||
|
||||
:param list names: domain names
|
||||
|
||||
:returns: tuple of the form (code, names) where
|
||||
code is a display exit code
|
||||
names is a list of names selected
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
code, tag = self.generic_menu(
|
||||
"Choose the names would you like to upgrade to HTTPS?",
|
||||
names, "Select the number of the name: ")
|
||||
|
|
@ -173,7 +299,29 @@ class FileDisplay(CommonDisplayMixin):
|
|||
# Make sure to return a list...
|
||||
return code, [names[tag]]
|
||||
|
||||
def display_certs(self, certs): # pylint: disable=missing-docstring
|
||||
def success_installation(self, domains):
|
||||
"""Display a box confirming the installation of HTTPS.
|
||||
|
||||
:param list domains: domain names which were enabled
|
||||
|
||||
"""
|
||||
side_frame = '*' * 79
|
||||
msg = textwrap.fill("Congratulations! You have successfully "
|
||||
"enabled %s!" % gen_https_names(domains))
|
||||
self.outfile.write("%s\n%s\n%s\n" % (side_frame, msg, side_frame))
|
||||
|
||||
|
||||
def display_certs(self, certs):
|
||||
"""Display certificates for revocation.
|
||||
|
||||
:param list certs: `list` of `dict` used throughout revoker.py
|
||||
|
||||
:returns: tuple of the form (code, selection) where
|
||||
code is a display exit code
|
||||
selection is the user's int selection
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
menu_choices = [(str(i+1), str(c["cn"]) + " - " + c["pub_key"] +
|
||||
" - " + str(c["not_before"])[:-6])
|
||||
for i, c in enumerate(certs)]
|
||||
|
|
@ -183,11 +331,20 @@ class FileDisplay(CommonDisplayMixin):
|
|||
self.outfile.write(textwrap.fill(
|
||||
"%s: %s - %s Signed (UTC): %s\n" % choice[:4]))
|
||||
|
||||
return self.__get_valid_int_ans("Revoke Number (c to cancel): ") - 1
|
||||
return self._get_valid_int_ans("Revoke Number (c to cancel): ") - 1
|
||||
|
||||
def __get_valid_int_ans(self, input_string):
|
||||
def _get_valid_int_ans(self, input_string):
|
||||
"""Get a numerical selection.
|
||||
|
||||
:param str input_string: Instructions for the user to make a selection.
|
||||
|
||||
:returns: tuple of the form (code, selection) where
|
||||
code is a display exit code
|
||||
selection is the user's int selection
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
valid_ans = False
|
||||
|
||||
e_msg = "Please input a number or the letter c to cancel\n"
|
||||
while not valid_ans:
|
||||
|
||||
|
|
@ -210,14 +367,15 @@ class FileDisplay(CommonDisplayMixin):
|
|||
|
||||
return code, selection
|
||||
|
||||
def success_installation(self, domains):
|
||||
# pylint: disable=missing-docstring
|
||||
side_frame = '*' * 79
|
||||
msg = textwrap.fill("Congratulations! You have successfully "
|
||||
"enabled %s!" % gen_https_names(domains))
|
||||
self.outfile.write("%s\n%s\n%s\n" % (side_frame, msg, side_frame))
|
||||
def confirm_revocation(self, cert):
|
||||
"""Confirm revocation screen.
|
||||
|
||||
def confirm_revocation(self, cert): # pylint: disable=missing-docstring
|
||||
:param dict cert: cert dict used throughout revoker.py
|
||||
|
||||
:returns: True if user would like to revoke, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
self.outfile.write("Are you sure you would like to revoke "
|
||||
"the following certificate:\n")
|
||||
self.outfile.write(cert_info_frame(cert))
|
||||
|
|
@ -225,39 +383,49 @@ class FileDisplay(CommonDisplayMixin):
|
|||
ans = raw_input("y/n")
|
||||
return ans.startswith('y') or ans.startswith('Y')
|
||||
|
||||
def more_info_cert(self, cert): # pylint: disable=missing-docstring
|
||||
def more_info_cert(self, cert):
|
||||
"""Displays more info about the cert.
|
||||
|
||||
:param dict cert: cert dict used throughout revoker.py
|
||||
|
||||
"""
|
||||
self.outfile.write("\nCertificate Information:\n")
|
||||
self.outfile.write(cert_info_frame(cert))
|
||||
|
||||
# Display exit codes
|
||||
OK = "ok"
|
||||
CANCEL = "cancel"
|
||||
HELP = "help"
|
||||
|
||||
|
||||
def cert_info_frame(cert): # pylint: disable=missing-docstring
|
||||
text = "-" * (WIDTH - 4) + "\n"
|
||||
def cert_info_frame(cert):
|
||||
"""Nicely frames a cert dict used in revoker.py"""
|
||||
text = "-" * (WIDTH - 4) + os.linesep
|
||||
text += cert_info_string(cert)
|
||||
text += "-" * (WIDTH - 4)
|
||||
return text
|
||||
|
||||
|
||||
def cert_info_string(cert): # pylint: disable=missing-docstring
|
||||
text = "Subject: %s\n" % cert["subject"]
|
||||
text += "SAN: %s\n" % cert["san"]
|
||||
text += "Issuer: %s\n" % cert["issuer"]
|
||||
text += "Public Key: %s\n" % cert["pub_key"]
|
||||
text += "Not Before: %s\n" % str(cert["not_before"])
|
||||
text += "Not After: %s\n" % str(cert["not_after"])
|
||||
text += "Serial Number: %s\n" % cert["serial"]
|
||||
text += "SHA1: %s\n" % cert["fingerprint"]
|
||||
text += "Installed: %s\n" % cert["installed"]
|
||||
return text
|
||||
def cert_info_string(cert):
|
||||
"""Turn a cert dict into a string."""
|
||||
text = []
|
||||
text.append("Subject: %s" % cert["subject"])
|
||||
text.append("SAN: %s" % cert["san"])
|
||||
text.append("Issuer: %s" % cert["issuer"])
|
||||
text.append("Public Key: %s" % cert["pub_key"])
|
||||
text.append("Not Before: %s" % str(cert["not_before"]))
|
||||
text.append("Not After: %s" % str(cert["not_after"]))
|
||||
text.append("Serial Number: %s" % cert["serial"])
|
||||
text.append("SHA1: %s" % cert["fingerprint"])
|
||||
text.append("Installed: %s" % cert["installed"])
|
||||
return os.linesep.join(text)
|
||||
|
||||
|
||||
def gen_https_names(domains):
|
||||
"""Returns a string of the https domains.
|
||||
|
||||
Domains are formatted nicely with https:// prepended to each.
|
||||
|
||||
"""
|
||||
result = ""
|
||||
if len(domains) > 2:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ import os
|
|||
import shutil
|
||||
import time
|
||||
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import CONFIG
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
|
||||
|
|
@ -100,26 +104,30 @@ class Reverter(object):
|
|||
raise errors.LetsEncryptReverterError(
|
||||
"Invalid directories in {0}".format(self.direc['backup']))
|
||||
|
||||
output = []
|
||||
for bkup in backups:
|
||||
print time.ctime(float(bkup))
|
||||
output.append(time.ctime(float(bkup)))
|
||||
cur_dir = os.path.join(self.direc['backup'], bkup)
|
||||
with open(os.path.join(cur_dir, "CHANGES_SINCE")) as changes_fd:
|
||||
print changes_fd.read()
|
||||
output.append(changes_fd.read())
|
||||
|
||||
print "Affected files:"
|
||||
output.append("Affected files:")
|
||||
with open(os.path.join(cur_dir, "FILEPATHS")) as paths_fd:
|
||||
filepaths = paths_fd.read().splitlines()
|
||||
for path in filepaths:
|
||||
print " {0}".format(path)
|
||||
output.append(" {0}".format(path))
|
||||
|
||||
if os.path.isfile(os.path.join(cur_dir, "NEW_FILES")):
|
||||
with open(os.path.join(cur_dir, "NEW_FILES")) as new_fd:
|
||||
print "New Configuration Files:"
|
||||
output.append("New Configuration Files:")
|
||||
filepaths = new_fd.read().splitlines()
|
||||
for path in filepaths:
|
||||
print " {0}".format(path)
|
||||
output.append(" {0}".format(path))
|
||||
|
||||
print "{0}".format(os.linesep)
|
||||
output.append(os.linesep)
|
||||
|
||||
zope.component.getUtility(interfaces.IDisplay).generic_notification(
|
||||
os.linesep.join(output), display.HEIGHT)
|
||||
|
||||
def add_to_temp_checkpoint(self, save_files, save_notes):
|
||||
"""Add files to temporary checkpoint
|
||||
|
|
|
|||
|
|
@ -331,12 +331,17 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
self.assertEqual(read_in(self.config2), "directive-dir2")
|
||||
self.assertFalse(os.path.isfile(config3))
|
||||
|
||||
def test_view_config_changes(self):
|
||||
@mock.patch("letsencrypt.client.client.zope.component.getUtility")
|
||||
def test_view_config_changes(self, mock_output):
|
||||
"""This is not strict as this is subject to change."""
|
||||
self._setup_three_checkpoints()
|
||||
# Just make sure it doesn't throw any errors.
|
||||
|
||||
# Make sure it doesn't throw any errors
|
||||
self.reverter.view_config_changes()
|
||||
|
||||
# Make sure notification is output
|
||||
self.assertEqual(mock_output().generic_notification.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.client.reverter.logging")
|
||||
def test_view_config_changes_no_backups(self, mock_logging):
|
||||
self.reverter.view_config_changes()
|
||||
|
|
|
|||
Loading…
Reference in a new issue