Repository: certbot/certbot

Issue:
Webroot Plugin: Cleanup Error
Hi! I'm using the letsencrypt client from a docker container with the following command:
`letsencrypt-auto certonly --agree-tos -m email@domain.com -d "domain.com" --webroot --webroot-path=/srv/www/letsencrypt`

The command fails with this error: 
`An unexpected error occurred:
OSError: [Errno 16] Device or resource busy: '/srv/www/letsencrypt/.well-known/acme-challenge'
Please see the logfiles in /var/log/letsencrypt for more details.`

The log file shows the exception that has been thrown here:
`2016-05-01 06:02:09,853:DEBUG:letsencrypt.main:Exiting abnormally:
Traceback (most recent call last):
  File "/root/.local/share/letsencrypt/bin/letsencrypt", line 11, in <module>
    sys.exit(main())
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/main.py", line 692, in main
    return config.func(config, plugins)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/main.py", line 509, in obtain_cert
    _, action = _auth_from_domains(le_client, config, domains, lineage)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/main.py", line 93, in _auth_from_domains
    lineage = le_client.obtain_and_enroll_certificate(domains)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/client.py", line 274, in obtain_and_enroll_certificate
    certr, chain, key, _ = self.obtain_certificate(domains)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/client.py", line 246, in obtain_certificate
    self.config.allow_subset_of_names)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/auth_handler.py", line 74, in get_authorizations
    self._respond(resp, best_effort)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/auth_handler.py", line 134, in _respond
    self._cleanup_challenges(active_achalls)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/auth_handler.py", line 266, in _cleanup_challenges
    self.auth.cleanup(achalls)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/plugins/webroot.py", line 234, in cleanup
    os.rmdir(root_path)
OSError: [Errno 16] Device or resource busy: '/srv/www/letsencrypt/.well-known/acme-challenge'`

Apparently it's related to the cleanup process. The interesting thing is that the text-file used for authorization actually is being deleted although the error implies otherwise. I commented out the following line:
https://github.com/letsencrypt/letsencrypt/blob/master/certbot/plugins/webroot.py#L234
Afterwards, everything worked as expected and even the text-file for authorization had been deleted (although that sounds strange to me). It looks like the removal of this file is being triggered multiple times.

I hope that helps you to narrow the problem down and fix it for further releases.



Discussion:
@LukasGentele
Is it /srv/www/letsencrypt/.well-known/acme-challenge mountpoint?


Resolving patch:
diff --git a/certbot/plugins/webroot.py b/certbot/plugins/webroot.py
--- a/certbot/plugins/webroot.py
+++ b/certbot/plugins/webroot.py
@@ -181,12 +181,8 @@ def _create_challenge_dirs(self):
                     os.chown(self.full_roots[name], stat_path.st_uid,
                              stat_path.st_gid)
                 except OSError as exception:
-                    if exception.errno == errno.EACCES:
-                        logger.debug("Insufficient permissions to change owner and uid - ignoring")
-                    else:
-                        raise errors.PluginError(
-                            "Couldn't create root for {0} http-01 "
-                            "challenge responses: {1}", name, exception)
+                    logger.info("Unable to change owner and uid of webroot directory")
+                    logger.debug("Error was: %s", exception)
 
             except OSError as exception:
                 if exception.errno != errno.EEXIST:
@@ -235,17 +231,9 @@ def cleanup(self, achalls):  # pylint: disable=missing-docstring
                     logger.debug("All challenges cleaned up, removing %s",
                                  root_path)
                 except OSError as exc:
-                    if exc.errno == errno.ENOTEMPTY:
-                        logger.debug("Challenges cleaned up but %s not empty",
-                                     root_path)
-                    elif exc.errno == errno.EACCES:
-                        logger.debug("Challenges cleaned up but no permissions for %s",
-                                     root_path)
-                    elif exc.errno == errno.ENOENT:
-                        logger.debug("Challenges cleaned up, %s does not exists",
-                                     root_path)
-                    else:
-                        raise
+                    logger.info(
+                        "Unable to clean up challenge directory %s", root_path)
+                    logger.debug("Error was: %s", exc)
 
 
 class _WebrootMapAction(argparse.Action):


Test patch:
diff --git a/certbot/plugins/webroot_test.py b/certbot/plugins/webroot_test.py
--- a/certbot/plugins/webroot_test.py
+++ b/certbot/plugins/webroot_test.py
@@ -138,15 +138,10 @@ def test_perform_reraises_other_errors(self):
         os.chmod(self.path, 0o700)
 
     @mock.patch("certbot.plugins.webroot.os.chown")
-    def test_failed_chown_eacces(self, mock_chown):
+    def test_failed_chown(self, mock_chown):
         mock_chown.side_effect = OSError(errno.EACCES, "msg")
         self.auth.perform([self.achall])  # exception caught and logged
 
-    @mock.patch("certbot.plugins.webroot.os.chown")
-    def test_failed_chown_not_eacces(self, mock_chown):
-        mock_chown.side_effect = OSError()
-        self.assertRaises(errors.PluginError, self.auth.perform, [])
-
     def test_perform_permissions(self):
         self.auth.prepare()
 
@@ -200,7 +195,7 @@ def test_cleanup_leftovers(self):
         os.rmdir(leftover_path)
 
     @mock.patch('os.rmdir')
-    def test_cleanup_permission_denied(self, mock_rmdir):
+    def test_cleanup_failure(self, mock_rmdir):
         self.auth.prepare()
         self.auth.perform([self.achall])
 
@@ -212,32 +207,6 @@ def test_cleanup_permission_denied(self, mock_rmdir):
         self.assertFalse(os.path.exists(self.validation_path))
         self.assertTrue(os.path.exists(self.root_challenge_path))
 
-    @mock.patch('os.rmdir')
-    def test_cleanup_oserror(self, mock_rmdir):
-        self.auth.prepare()
-        self.auth.perform([self.achall])
-
-        os_error = OSError()
-        os_error.errno = errno.EPERM
-        mock_rmdir.side_effect = os_error
-
-        self.assertRaises(OSError, self.auth.cleanup, [self.achall])
-        self.assertFalse(os.path.exists(self.validation_path))
-        self.assertTrue(os.path.exists(self.root_challenge_path))
-
-    @mock.patch('os.rmdir')
-    def test_cleanup_file_not_exists(self, mock_rmdir):
-        self.auth.prepare()
-        self.auth.perform([self.achall])
-
-        os_error = OSError()
-        os_error.errno = errno.ENOENT
-        mock_rmdir.side_effect = os_error
-
-        self.auth.cleanup([self.achall])
-        self.assertFalse(os.path.exists(self.validation_path))
-        self.assertTrue(os.path.exists(self.root_challenge_path))
-
 
 class WebrootActionTest(unittest.TestCase):
     """Tests for webroot argparse actions."""