Fixed print, fixed logging, made display work

This commit is contained in:
James Kasten 2015-01-26 04:49:40 -08:00
parent 2db2060f85
commit 243cc4f9fb
3 changed files with 240 additions and 59 deletions

View file

@ -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:

View file

@ -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

View file

@ -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()