Repository: certbot/certbot

Issue:
OSError: [Errno 22] Invalid argument
hi,

my renewal does not work. I'm running Ubuntu 14.04 with python 2.7.6 on my machine. My config file in `/etc/letsencrypt/cli.ini` looks as follows (I changed the domain names):

```
rsa-key-size = 4096
server = https://acme-staging.api.letsencrypt.org/directory
email = mail@mydomain.com
domains = mydomain.com
text = True
authenticator = webroot
webroot-path = /var/www/html
agree-tos = True
renew-by-default = True
```

If I now run (as root) the command `/opt/letsencrypt/letsencrypt-auto certonly --config /etc/letsencrypt/cli.ini` I get the following error:

```
Updating letsencrypt and virtual environment dependencies......
Requesting root privileges to run with virtualenv: /root/.local/share/letsencrypt/bin/letsencrypt certonly -a webroot --config /etc/letsencrypt/cli.ini
An unexpected error occurred:
OSError: [Errno 22] Invalid argument: '/etc/letsencrypt/live/mydomain.com/cert.pem'
Please see the logfiles in /var/log/letsencrypt for more details.
```

**/var/log/letsencrypt/letsencrypt.log**

```
2016-01-01 18:28:39,820:DEBUG:letsencrypt.cli:Root logging level set at 30
2016-01-01 18:28:39,820:INFO:letsencrypt.cli:Saving debug log to /var/log/letsencrypt/letsencrypt.log
2016-01-01 18:28:39,820:DEBUG:letsencrypt.cli:letsencrypt version: 0.1.1
2016-01-01 18:28:39,820:DEBUG:letsencrypt.cli:Arguments: ['-a', 'webroot']
2016-01-01 18:28:39,820:DEBUG:letsencrypt.cli:Discovered plugins: PluginsRegistry(PluginEntryPoint#apache,PluginEntryPoint#webroot,PluginEntryPoint#null,PluginEntryPoint#manual,PluginEntryPoint#standalone)
2016-01-01 18:28:39,821:DEBUG:letsencrypt.cli:Requested authenticator webroot and installer None
2016-01-01 18:28:39,823:DEBUG:letsencrypt.plugins.webroot:Creating root challenges validation dir at /usr/share/nginx/html/.well-known/acme-challenge
2016-01-01 18:28:39,823:DEBUG:letsencrypt.plugins.webroot:Creating root challenges validation dir at /usr/share/nginx/html/.well-known/acme-challenge
2016-01-01 18:28:39,823:DEBUG:letsencrypt.plugins.webroot:Creating root challenges validation dir at /usr/share/nginx/html/.well-known/acme-challenge
2016-01-01 18:28:39,823:DEBUG:letsencrypt.plugins.webroot:Creating root challenges validation dir at /usr/share/nginx/html/.well-known/acme-challenge
2016-01-01 18:28:39,824:DEBUG:letsencrypt.plugins.webroot:Creating root challenges validation dir at /usr/share/nginx/html/.well-known/acme-challenge
2016-01-01 18:28:39,824:DEBUG:letsencrypt.display.ops:Single candidate plugin: * webroot
Description: Webroot Authenticator
Interfaces: IAuthenticator, IPlugin
Entry point: webroot = letsencrypt.plugins.webroot:Authenticator
Initialized: <letsencrypt.plugins.webroot.Authenticator object at 0x7f67d00d9450>
Prep: True
2016-01-01 18:28:39,824:DEBUG:letsencrypt.cli:Selected authenticator <letsencrypt.plugins.webroot.Authenticator object at 0x7f67d00d9450> and installer None
2016-01-01 18:28:39,839:DEBUG:letsencrypt.cli:Picked account: <Account(xxxxxxxxxxxxxxxxxxxxxxxx)>
2016-01-01 18:28:39,839:DEBUG:root:Sending GET request to https://acme-staging.api.letsencrypt.org/directory. args: (), kwargs: {}
2016-01-01 18:28:39,842:INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): acme-staging.api.letsencrypt.org
2016-01-01 18:28:40,390:DEBUG:requests.packages.urllib3.connectionpool:"GET /directory HTTP/1.1" 200 279
2016-01-01 18:28:40,392:DEBUG:root:Received <Response [200]>. Headers: {'Content-Length': '279', 'Expires': 'Fri, 01 Jan 2016 18:28:41 GMT', 'Strict-Transport-Security': 'max-age=604800', 'Server': 'nginx', 'Connection': 'keep-alive', 'Pragma': 'no-cache', 'Cache-Control': 'max-age=0, no-cache, no-store', 'Date': 'Fri, 01 Jan 2016 18:28:41 GMT', 'X-Frame-Options': 'DENY', 'Content-Type': 'application/json', 'Replay-Nonce': 'QSF70cRfDBqwuL0GJphAiU-cdxGUlyB1Z-gIjJH3x4A'}. Content: '{"new-authz":"https://acme-staging.api.letsencrypt.org/acme/new-authz","new-cert":"https://acme-staging.api.letsencrypt.org/acme/new-cert","new-reg":"https://acme-staging.api.letsencrypt.org/acme/new-reg","revoke-cert":"https://acme-staging.api.letsencrypt.org/acme/revoke-cert"}'
2016-01-01 18:28:40,393:DEBUG:acme.client:Received response <Response [200]> (headers: {'Content-Length': '279', 'Expires': 'Fri, 01 Jan 2016 18:28:41 GMT', 'Strict-Transport-Security': 'max-age=604800', 'Server': 'nginx', 'Connection': 'keep-alive', 'Pragma': 'no-cache', 'Cache-Control': 'max-age=0, no-cache, no-store', 'Date': 'Fri, 01 Jan 2016 18:28:41 GMT', 'X-Frame-Options': 'DENY', 'Content-Type': 'application/json', 'Replay-Nonce': 'QSF70cRfDBqwuL0GJphAiU-cdxGUlyB1Z-gIjJH3x4A'}): '{"new-authz":"https://acme-staging.api.letsencrypt.org/acme/new-authz","new-cert":"https://acme-staging.api.letsencrypt.org/acme/new-cert","new-reg":"https://acme-staging.api.letsencrypt.org/acme/new-reg","revoke-cert":"https://acme-staging.api.letsencrypt.org/acme/revoke-cert"}'
2016-01-01 18:28:40,394:DEBUG:letsencrypt.cli: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/cli.py", line 1396, in main
    return args.func(args, config, plugins)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/cli.py", line 598, in obtain_cert
    _auth_from_domains(le_client, config, domains)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/cli.py", line 379, in _auth_from_domains
    action, lineage = _treat_as_renewal(config, domains)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/cli.py", line 235, in _treat_as_renewal
    ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains)
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/cli.py", line 204, in _find_duplicative_certs
    candidate_names = set(candidate_lineage.names())
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/storage.py", line 456, in names
    target = self.current_target("cert")
  File "/root/.local/share/letsencrypt/local/lib/python2.7/site-packages/letsencrypt/storage.py", line 273, in current_target
    target = os.readlink(link)
OSError: [Errno 22] Invalid argument: '/etc/letsencrypt/live/mydomain.com/cert.pem'
```

Do you know how I can solve this problem?

Regards,
Jan Bücker



Discussion:
I ran into the same issue and believe the following patch addresses this: https://github.com/letsencrypt/letsencrypt/issues/2088

The original bug report is possibly a duplicate of #1668.  The patch in #2088 appears to be for cases where `/etc/letsencrypt/live/domain.com/cert.pem` is not a symlink, but it should always be a symlink to the latest cert in that lineage...

Yes, the file is not a symlink. I think this is the result of moving to another server. Nevertheless, the fix in #2088 does not seem to work, since I'm still getting the same error after changing the `storage.py` file. This surprises me because the code snippets actually works in the python cli.


Resolving patch:
diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py
--- a/letsencrypt/storage.py
+++ b/letsencrypt/storage.py
@@ -112,6 +112,21 @@ def update_configuration(lineagename, target, cli_config):
     return configobj.ConfigObj(config_filename)
 
 
+def get_link_target(link):
+    """Get an absolute path to the target of link.
+
+    :param str link: Path to a symbolic link
+
+    :returns: Absolute path to the target of link
+    :rtype: str
+
+    """
+    target = os.readlink(link)
+    if not os.path.isabs(target):
+        target = os.path.join(os.path.dirname(link), target)
+    return os.path.abspath(target)
+
+
 class RenewableCert(object):  # pylint: disable=too-many-instance-attributes
     """Renewable certificate.
 
@@ -194,13 +209,15 @@ def __init__(self, config_filename, cli_config):
 
     def _check_symlinks(self):
         """Raises an exception if a symlink doesn't exist"""
-        def check(link):
-            """Checks if symlink points to a file that exists"""
-            return os.path.exists(os.path.realpath(link))
         for kind in ALL_FOUR:
-            if not check(getattr(self, kind)):
+            link = getattr(self, kind)
+            if not os.path.islink(link):
                 raise errors.CertStorageError(
-                        "link: {0} does not exist".format(getattr(self, kind)))
+                    "expected {0} to be a symlink".format(link))
+            target = get_link_target(link)
+            if not os.path.exists(target):
+                raise errors.CertStorageError("target {0} of symlink {1} does "
+                                              "not exist".format(target, link))
 
     def _consistent(self):
         """Are the files associated with this lineage self-consistent?
@@ -225,10 +242,7 @@ def _consistent(self):
                 return False
         for kind in ALL_FOUR:
             link = getattr(self, kind)
-            where = os.path.dirname(link)
-            target = os.readlink(link)
-            if not os.path.isabs(target):
-                target = os.path.join(where, target)
+            target = get_link_target(link)
 
             # Each element's link must point within the cert lineage's
             # directory within the official archive directory
@@ -343,10 +357,7 @@ def current_target(self, kind):
             logger.debug("Expected symlink %s for %s does not exist.",
                          link, kind)
             return None
-        target = os.readlink(link)
-        if not os.path.isabs(target):
-            target = os.path.join(os.path.dirname(link), target)
-        return os.path.abspath(target)
+        return get_link_target(link)
 
     def current_version(self, kind):
         """Returns numerical version of the specified item.


Test patch:
diff --git a/letsencrypt/tests/storage_test.py b/letsencrypt/tests/storage_test.py
--- a/letsencrypt/tests/storage_test.py
+++ b/letsencrypt/tests/storage_test.py
@@ -687,6 +687,10 @@ def test_missing_cert(self):
         self.assertRaises(errors.CertStorageError,
                           storage.RenewableCert,
                           self.config.filename, self.cli_config)
+        os.symlink("missing", self.config[ALL_FOUR[0]])
+        self.assertRaises(errors.CertStorageError,
+                          storage.RenewableCert,
+                          self.config.filename, self.cli_config)
 
 
 if __name__ == "__main__":