Repository: freedomofpress/securedrop

Issue:
Add `daily_reboot_time` option to securedrop-admin sdconfig and document it
## Description

This is a followup to issue #1515. In the corresponding PR (#1590), the ability to manually edit `prod_specific.yml` to customize the `daily_reboot_time` was added. This enabled admins to control when their SecureDrop instances will upgrade and reboot. 

However, we now discourage direct edits to `prod_specific.yml` in favor of using `./securedrop-admin sdconfig`. Additionally, the ability to customize the daily reboot time via `daily_reboot_time` is currently not documented.

We should add the `daily_reboot_time` option to the `./securedrop-admin sdconfig` series of prompts and add some documentation to the Install and Administrator Guides explaining what `daily_reboot_time` does and how to enable it. 

## User Stories

As a SecureDrop admin, I want to easily customize the reboot time so that I can schedule my SecureDrop upgrade while I am at work in case issues arise.
Add `daily_reboot_time` option to securedrop-admin sdconfig and document it
## Description

This is a followup to issue #1515. In the corresponding PR (#1590), the ability to manually edit `prod_specific.yml` to customize the `daily_reboot_time` was added. This enabled admins to control when their SecureDrop instances will upgrade and reboot. 

However, we now discourage direct edits to `prod_specific.yml` in favor of using `./securedrop-admin sdconfig`. Additionally, the ability to customize the daily reboot time via `daily_reboot_time` is currently not documented.

We should add the `daily_reboot_time` option to the `./securedrop-admin sdconfig` series of prompts and add some documentation to the Install and Administrator Guides explaining what `daily_reboot_time` does and how to enable it. 

## User Stories

As a SecureDrop admin, I want to easily customize the reboot time so that I can schedule my SecureDrop upgrade while I am at work in case issues arise.


Discussion:

Resolving patch:
diff --git a/admin/securedrop_admin/__init__.py b/admin/securedrop_admin/__init__.py
--- a/admin/securedrop_admin/__init__.py
+++ b/admin/securedrop_admin/__init__.py
@@ -51,6 +51,13 @@ def validate(self, document):
             raise ValidationError(
                 message="Must not be an empty string")
 
+    class ValidateTime(Validator):
+        def validate(self, document):
+            if document.text.isdigit() and int(document.text) in range(0, 24):
+                return True
+            raise ValidationError(
+                message="Must be an integer between 0 and 23")
+
     class ValidateUser(Validator):
         def validate(self, document):
             text = document.text
@@ -210,6 +217,10 @@ def __init__(self, args):
              u'Username for SSH access to the servers',
              SiteConfig.ValidateUser(),
              None],
+            ['daily_reboot_time', 4, int,
+             u'Daily reboot time of the server (24-hour clock)',
+             SiteConfig.ValidateTime(),
+             int],
             ['app_ip', '10.20.2.2', str,
              u'Local IPv4 address for the Application Server',
              SiteConfig.ValidateIP(),


Test patch:
diff --git a/admin/tests/test_securedrop-admin.py b/admin/tests/test_securedrop-admin.py
--- a/admin/tests/test_securedrop-admin.py
+++ b/admin/tests/test_securedrop-admin.py
@@ -157,6 +157,21 @@ def test_validate_not_empty(self):
         with pytest.raises(ValidationError):
             validator.validate(Document(''))
 
+    def test_validate_time(self):
+        validator = securedrop_admin.SiteConfig.ValidateTime()
+
+        assert validator.validate(Document('4'))
+        with pytest.raises(ValidationError):
+            validator.validate(Document(''))
+        with pytest.raises(ValidationError):
+            validator.validate(Document('four'))
+        with pytest.raises(ValidationError):
+            validator.validate(Document('4.30'))
+        with pytest.raises(ValidationError):
+            validator.validate(Document('25'))
+        with pytest.raises(ValidationError):
+            validator.validate(Document('-4'))
+
     def test_validate_ossec_username(self):
         validator = securedrop_admin.SiteConfig.ValidateOSSECUsername()
 
@@ -484,6 +499,7 @@ def verify_desc_consistency_allow_empty(self, site_config, desc):
     verify_prompt_ossec_alert_email = verify_prompt_not_empty
     verify_prompt_smtp_relay = verify_prompt_not_empty
     verify_prompt_smtp_relay_port = verify_desc_consistency
+    verify_prompt_daily_reboot_time = verify_desc_consistency
     verify_prompt_sasl_domain = verify_desc_consistency_allow_empty
     verify_prompt_sasl_username = verify_prompt_not_empty
     verify_prompt_sasl_password = verify_prompt_not_empty