Repository: certbot/certbot

Issue:
 --keep-until-expiring requires interaction if no certificate update 
Running a command of `./letsencrypt-auto --config /opt/letsencrypt.ini certonly --keep-until-expiring --agree-tos -d my.domain.com` with configuration file contents that includes the option `text = True` results in prompted input if certificates are not yet due for removal, breaking automated scripts.
## STR:
1. Request certificates using let's encrypt (using the options provided below).
2. Request certificates again using `--keep-until-expiring` flag set and a configuration file that specifies `text = True`, the same that should have been used for the first request:
   1. An example command would be `./letsencrypt-auto --config /opt/letsencrypt.ini certonly --keep-until-expiring --agree-tos -d my.domain.com`
   2. with a configuration file of:
   
   ```
   # Let's Encrypt Configuration with defaults
   # All flags used by the client can be configured here. Run Let's Encrypt with
   # "--help" to learn more about the available options.
   
   # Use a 4096 bit RSA key instead of 2048
   rsa-key-size = 4096
   
   # Uncomment and update to register with the specified e-mail address
   email = EMAIL_HERE@EMAIL_HERE
   
   # Uncomment and update to generate certificates for the specified
   # domains.
   # domains = example.com, www.example.com
   # Always provide domains via env. See letsencrypt-run.sh
   
   # Uncomment to use a text interface instead of ncurses
   text = True
   
   # Uncomment to use the standalone authenticator on port 443
   # authenticator = standalone
   # standalone-supported-challenges = tls-sni-01
   
   # Uncomment to use the webroot authenticator. Replace webroot-path with the
   # path to the public_html / webroot folder being served by your web server.
   authenticator = webroot
   webroot-path = /var/www/challenges
   ```
## Expected:
1. Let's Encrypt client prints:

```
------------------------------------------------------------------------------- 
Certificate not yet due for renewal; no action taken.                           
------------------------------------------------------------------------------- 
```

and does not prompt for input afterward, allowing the client to exit with a code of `0`.
## Actual:
1. Let's Encrypt client prints:

```
------------------------------------------------------------------------------- 
Certificate not yet due for renewal; no action taken.                           
------------------------------------------------------------------------------- 
Press Enter to Continue
```

and requires interaction from a tty before exiting cleanly. 
## Environment
- OS = Ubuntu 14.04.2 
- Webserver = nginx, but using certonly since acme-challenges are successfully handled through an existing nginx server. The `letsencrypt.ini` file specifies the correct challenge directory for the client by providing the options 
  - 
  
  ```
  authenticator = webroot
  webroot-path = /var/www/challenges
  ```
### Notes

Let's Encrypt was installed by running and then calling:

```
git clone https://github.com/letsencrypt/letsencrypt
./letsencrypt-auto --config /opt/letsencrypt.ini certonly --keep-until-expiring --agree-tos -d my.domain.com
```



Discussion:
Thank you for taking the time to write such a detailed bug report.

It looks like this bug was introduced in our last release. It will be fixed in our next release, but in meantime you can use the non-interactive option to ensure the client won't block on user input. From the output of `letsencrypt --help all`:

```
  -n, --non-interactive, --noninteractive
                        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 (default: False)
```

If you set this flag on the command line or in your config file, the client will not prompt and you'll see your expected behavior.

Thanks for providing a workaround! I'll implement that in the meantime. Sorry, I didn't look in the output, I tried searching through the documentation online for the `--keep-until-expiring` details and the `text` configuration option. 😅


Resolving patch:
diff --git a/certbot/main.py b/certbot/main.py
--- a/certbot/main.py
+++ b/certbot/main.py
@@ -528,7 +528,7 @@ def obtain_cert(config, plugins, lineage=None):
             notify("new certificate deployed with reload of {0} server; fullchain is {1}".format(
                    config.installer, lineage.fullchain), pause=False)
     elif action == "reinstall" and config.verb == "certonly":
-        notify("Certificate not yet due for renewal; no action taken.")
+        notify("Certificate not yet due for renewal; no action taken.", pause=False)
     _suggest_donation_if_appropriate(config, action)
 
 


Test patch:
diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py
new file mode 100644
--- /dev/null
+++ b/certbot/tests/main_test.py
@@ -0,0 +1,48 @@
+"""Tests for certbot.main."""
+import unittest
+
+
+import mock
+
+
+from certbot import cli
+from certbot import configuration
+from certbot.plugins import disco as plugins_disco
+
+
+class ObtainCertTest(unittest.TestCase):
+    """Tests for certbot.main.obtain_cert."""
+
+    def setUp(self):
+        self.get_utility_patch = mock.patch(
+            'certbot.main.zope.component.getUtility')
+        self.mock_get_utility = self.get_utility_patch.start()
+
+    def tearDown(self):
+        self.get_utility_patch.stop()
+
+    def _call(self, args):
+        plugins = plugins_disco.PluginsRegistry.find_all()
+        config = configuration.NamespaceConfig(
+            cli.prepare_and_parse_args(plugins, args))
+
+        from certbot import main
+        with mock.patch('certbot.main._init_le_client') as mock_init:
+            main.obtain_cert(config, plugins)
+
+        return mock_init()  # returns the client
+
+    @mock.patch('certbot.main._auth_from_domains')
+    def test_no_reinstall_text_pause(self, mock_auth):
+        mock_notification = self.mock_get_utility().notification
+        mock_notification.side_effect = self._assert_no_pause
+        mock_auth.return_value = (mock.ANY, 'reinstall')
+        self._call('certonly --webroot -d example.com -t'.split())
+
+    def _assert_no_pause(self, message, height=42, pause=True):
+        # pylint: disable=unused-argument
+        self.assertFalse(pause)
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover