Repository: certbot/certbot

Issue:
Packaging doesn't preserve symlinks, use os.symlink
While preparing dev release packages I stumbled upon `letsencrypt_apache` test errors that don't show up in our standard testing suite. The difference is that I packaged everything (using `python setup.py sdist`/`python setup.py bdist_wheel`), installed in fresh venv (`pip install *.tar.gz`) and run "standalone" `nostests`:

```

+ nosetests letsencrypt acme letsencrypt_apache letsencrypt_nginx letshelp_letsencrypt
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................FF........E.F...........F........................................................................................................
======================================================================
ERROR: Make sure all vhosts are being properly found.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 101, in test_get_virtual_hosts
    raise Exception("Missed: %s" % vhost)  # pragma: no cover
Exception: Missed: File: /tmp/tmp7P5gMbtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf
Vhost path: /files/tmp/tmp7P5gMbtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf/VirtualHost
Addresses: *:80
Name: letsencrypt.demo
Aliases:
TLS Enabled: No
Site Enabled: No

======================================================================
FAIL: test_find_best_vhost (letsencrypt_apache.tests.configurator_test.TwoVhost80Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 141, in test_find_best_vhost
    self.vh_truth[3], self.config._find_best_vhost("letsencrypt.demo"))
AssertionError: <letsencrypt_apache.obj.VirtualHost object at 0x7f1169064b10> != <letsencrypt_apache.obj.VirtualHost object at 0x7f1169064790>

======================================================================
FAIL: test_find_best_vhost_default (letsencrypt_apache.tests.configurator_test.TwoVhost80Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 165, in test_find_best_vhost_default
    self.config._find_best_vhost("example.demo"), self.vh_truth[2])
AssertionError: <letsencrypt_apache.obj.VirtualHost object at 0x7f1169033210> != <letsencrypt_apache.obj.VirtualHost object at 0x7f1169033250>

======================================================================
FAIL: Test if site is enabled.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 180, in test_is_site_enabled
    self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep))
AssertionError: False is not true

======================================================================
FAIL: test_redirect_with_conflict (letsencrypt_apache.tests.configurator_test.TwoVhost80Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 535, in test_redirect_with_conflict
    errors.PluginError, self.config._enable_redirect, ssl_vh, "")
AssertionError: PluginError not raised
-------------------- >> begin captured logging << --------------------
letsencrypt_apache.configurator: DEBUG: Did not find http version of ssl virtual host attempting to create
letsencrypt_apache.configurator: INFO: Created redirect file: le-redirect.conf
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 883 tests in 31.251s

FAILED (errors=1, failures=4)
```

Any ideas, @jdkasten?

Packaging doesn't preserve symlinks, use os.symlink
While preparing dev release packages I stumbled upon `letsencrypt_apache` test errors that don't show up in our standard testing suite. The difference is that I packaged everything (using `python setup.py sdist`/`python setup.py bdist_wheel`), installed in fresh venv (`pip install *.tar.gz`) and run "standalone" `nostests`:

```

+ nosetests letsencrypt acme letsencrypt_apache letsencrypt_nginx letshelp_letsencrypt
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................FF........E.F...........F........................................................................................................
======================================================================
ERROR: Make sure all vhosts are being properly found.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 101, in test_get_virtual_hosts
    raise Exception("Missed: %s" % vhost)  # pragma: no cover
Exception: Missed: File: /tmp/tmp7P5gMbtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf
Vhost path: /files/tmp/tmp7P5gMbtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf/VirtualHost
Addresses: *:80
Name: letsencrypt.demo
Aliases:
TLS Enabled: No
Site Enabled: No

======================================================================
FAIL: test_find_best_vhost (letsencrypt_apache.tests.configurator_test.TwoVhost80Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 141, in test_find_best_vhost
    self.vh_truth[3], self.config._find_best_vhost("letsencrypt.demo"))
AssertionError: <letsencrypt_apache.obj.VirtualHost object at 0x7f1169064b10> != <letsencrypt_apache.obj.VirtualHost object at 0x7f1169064790>

======================================================================
FAIL: test_find_best_vhost_default (letsencrypt_apache.tests.configurator_test.TwoVhost80Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 165, in test_find_best_vhost_default
    self.config._find_best_vhost("example.demo"), self.vh_truth[2])
AssertionError: <letsencrypt_apache.obj.VirtualHost object at 0x7f1169033210> != <letsencrypt_apache.obj.VirtualHost object at 0x7f1169033250>

======================================================================
FAIL: Test if site is enabled.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 180, in test_is_site_enabled
    self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep))
AssertionError: False is not true

======================================================================
FAIL: test_redirect_with_conflict (letsencrypt_apache.tests.configurator_test.TwoVhost80Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/tmp.aEsCqPpghg/venv/local/lib/python2.7/site-packages/letsencrypt_apache/tests/configurator_test.py", line 535, in test_redirect_with_conflict
    errors.PluginError, self.config._enable_redirect, ssl_vh, "")
AssertionError: PluginError not raised
-------------------- >> begin captured logging << --------------------
letsencrypt_apache.configurator: DEBUG: Did not find http version of ssl virtual host attempting to create
letsencrypt_apache.configurator: INFO: Created redirect file: le-redirect.conf
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 883 tests in 31.251s

FAILED (errors=1, failures=4)
```

Any ideas, @jdkasten?



Discussion:
```
(Pdb) print self.vh_truth[3]
File: /tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf
Vhost path: /files/tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf/VirtualHost
Addresses: *:80
Name: letsencrypt.demo
Aliases: 
TLS Enabled: No
Site Enabled: Yes
(Pdb) self.test_get_virtual_hosts()
*** Exception: Missed: File: /tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf
Vhost path: /files/tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf/VirtualHost
Addresses: *:80
Name: letsencrypt.demo
Aliases: 
TLS Enabled: No
Site Enabled: No
```

There is a difference in "Site Enabled"

More debug info:

```
(venv)jakub@iks:/tmp/tmpxY6XbXtemp$ tree
.
└── debian_apache_2_4
    └── two_vhost_80
        ├── apache2
        │   ├── apache2.conf
        │   ├── conf-available
        │   │   ├── bad_conf_file.conf
        │   │   ├── other-vhosts-access-log.conf
        │   │   ├── security.conf
        │   │   └── serve-cgi-bin.conf
        │   ├── conf-enabled
        │   │   ├── other-vhosts-access-log.conf
        │   │   ├── security.conf
        │   │   └── serve-cgi-bin.conf
        │   ├── envvars
        │   ├── mods-available
        │   │   ├── rewrite.load
        │   │   ├── ssl.conf
        │   │   └── ssl.load
        │   ├── mods-enabled
        │   ├── ports.conf
        │   ├── sites-available
        │   │   ├── 000-default.conf
        │   │   ├── default-ssl.conf
        │   │   ├── encryption-example.conf
        │   │   └── letsencrypt.conf
        │   └── sites-enabled
        │       ├── 000-default.conf
        │       ├── encryption-example.conf
        │       └── letsencrypt.conf
        └── sites

9 directories, 21 files
```

```
(Pdb) vhosts = self.config.get_virtual_hosts()
(Pdb) [vhosts[y].enabled for y in range(4)]
[False, False, False, False]
```

The problem is in [ApacheConfigurator.is_site_enabled](https://github.com/letsencrypt/letsencrypt/blob/523dba0b999ec46daf8c74edbfe1c29a3b43d8d2/letsencrypt-apache/letsencrypt_apache/configurator.py#L929-L945):

``` python
def is_site_enabled(self, avail_fp):
    """Checks to see if the given site is enabled.

    .. todo:: fix hardcoded sites-enabled, check os.path.samefile

    :param str avail_fp: Complete file path of available site
    :returns: Success
    :rtype: bool

    """
    enabled_dir = os.path.join(self.parser.root, "sites-enabled")
    for entry in os.listdir(enabled_dir):
        if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp:
            return True

    return False
```

`avail_fp` is a full path, s.t. `"sites-available" in avail_fp is True` (e.g. `u'/tmp/tmpwZlGgGtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf'`). There is no possible way `os.path.realpath(os.path.join(enabled_dir, entry)` wold ever produce a path that contains `sites-available`...

How is it possible that this wasn't caught by our testing suite?

Oh, this is `os.path.realpath`... But my tree doesn't have any symlinks :(

Okay, seems like wheels don't like symlinks :(

Installing from tarball (where symlinks are present) also doesn't install them as symlinks, even when `zip_safe=False`. I think we should just recreate them at runtime instead of relying on Python packaging tools

```
(Pdb) print self.vh_truth[3]
File: /tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf
Vhost path: /files/tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf/VirtualHost
Addresses: *:80
Name: letsencrypt.demo
Aliases: 
TLS Enabled: No
Site Enabled: Yes
(Pdb) self.test_get_virtual_hosts()
*** Exception: Missed: File: /tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf
Vhost path: /files/tmp/tmpxY6XbXtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf/VirtualHost
Addresses: *:80
Name: letsencrypt.demo
Aliases: 
TLS Enabled: No
Site Enabled: No
```

There is a difference in "Site Enabled"

More debug info:

```
(venv)jakub@iks:/tmp/tmpxY6XbXtemp$ tree
.
└── debian_apache_2_4
    └── two_vhost_80
        ├── apache2
        │   ├── apache2.conf
        │   ├── conf-available
        │   │   ├── bad_conf_file.conf
        │   │   ├── other-vhosts-access-log.conf
        │   │   ├── security.conf
        │   │   └── serve-cgi-bin.conf
        │   ├── conf-enabled
        │   │   ├── other-vhosts-access-log.conf
        │   │   ├── security.conf
        │   │   └── serve-cgi-bin.conf
        │   ├── envvars
        │   ├── mods-available
        │   │   ├── rewrite.load
        │   │   ├── ssl.conf
        │   │   └── ssl.load
        │   ├── mods-enabled
        │   ├── ports.conf
        │   ├── sites-available
        │   │   ├── 000-default.conf
        │   │   ├── default-ssl.conf
        │   │   ├── encryption-example.conf
        │   │   └── letsencrypt.conf
        │   └── sites-enabled
        │       ├── 000-default.conf
        │       ├── encryption-example.conf
        │       └── letsencrypt.conf
        └── sites

9 directories, 21 files
```

```
(Pdb) vhosts = self.config.get_virtual_hosts()
(Pdb) [vhosts[y].enabled for y in range(4)]
[False, False, False, False]
```

The problem is in [ApacheConfigurator.is_site_enabled](https://github.com/letsencrypt/letsencrypt/blob/523dba0b999ec46daf8c74edbfe1c29a3b43d8d2/letsencrypt-apache/letsencrypt_apache/configurator.py#L929-L945):

``` python
def is_site_enabled(self, avail_fp):
    """Checks to see if the given site is enabled.

    .. todo:: fix hardcoded sites-enabled, check os.path.samefile

    :param str avail_fp: Complete file path of available site
    :returns: Success
    :rtype: bool

    """
    enabled_dir = os.path.join(self.parser.root, "sites-enabled")
    for entry in os.listdir(enabled_dir):
        if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp:
            return True

    return False
```

`avail_fp` is a full path, s.t. `"sites-available" in avail_fp is True` (e.g. `u'/tmp/tmpwZlGgGtemp/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf'`). There is no possible way `os.path.realpath(os.path.join(enabled_dir, entry)` wold ever produce a path that contains `sites-available`...

How is it possible that this wasn't caught by our testing suite?

Oh, this is `os.path.realpath`... But my tree doesn't have any symlinks :(

Okay, seems like wheels don't like symlinks :(

Installing from tarball (where symlinks are present) also doesn't install them as symlinks, even when `zip_safe=False`. I think we should just recreate them at runtime instead of relying on Python packaging tools


Resolving patch:
diff --git a/letsencrypt-apache/letsencrypt_apache/configurator.py b/letsencrypt-apache/letsencrypt_apache/configurator.py
--- a/letsencrypt-apache/letsencrypt_apache/configurator.py
+++ b/letsencrypt-apache/letsencrypt_apache/configurator.py
@@ -1,5 +1,6 @@
 """Apache Configuration based off of Augeas Configurator."""
 # pylint: disable=too-many-lines
+import filecmp
 import itertools
 import logging
 import os
@@ -945,9 +946,11 @@ def is_site_enabled(self, avail_fp):
         """
         enabled_dir = os.path.join(self.parser.root, "sites-enabled")
         for entry in os.listdir(enabled_dir):
-            if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp:
-                return True
-
+            try:
+                if filecmp.cmp(avail_fp, os.path.join(enabled_dir, entry)):
+                    return True
+            except OSError:
+                pass
         return False
 
     def enable_site(self, vhost):


Test patch: