Changeset - c8d4251a6ea5
[Not reviewed]
0 11 0
Branko Majic (branko) - 7 years ago 2018-07-30 12:54:28
branko@majic.rs
MAR-131: Added support for specifying Python version in wsgi_website role:

- Introduced additional role parameter for specifying the Python
version.
- Updated tests to verify new functionality.
- Fixed existing tests to account for differences between Python 2 and
Python 3 - including changes to WSGI test applications.
- Updated documentation, documenting new parameter and fixing one
minor typo.
- Updated release notes.
- Bumped default version of Gunicorn/futures used.
11 files changed with 57 insertions and 12 deletions:
0 comments (0 inline, 0 general)
docs/releasenotes.rst
Show inline comments
 
@@ -36,12 +36,17 @@ New features/improvements:
 
  * Certificate expiration check is less verbose. No mails are sent
 
    out any longer in case no certificates have been configured for
 
    checking, nor in cases where all certificates have passed the
 
    check. E.g. mails are sent out only in case some of the configured
 
    certificates will expire within next 30 days.
 

	
 
* ``wsgi_website`` role
 

	
 
  * Support for specifying Python version for Python virtual
 
    environment.
 

	
 

	
 
2.0.0
 
-----
 

	
 
Upgrade to Ansible 2.3.x, minor bug fixes and updates needed for the upgrade.
 

	
docs/rolereference.rst
Show inline comments
 
@@ -1642,15 +1642,16 @@ The role implements the following:
 
* Creates a base directory where the website-specific code and data should be
 
  stored at.
 
* Adds nginx to website's group, so nginx could read the necessary files.
 
* Adds website administrator to website's group, so administrator could manage
 
  the code and data.
 
* Installs additional packages required for running the role (as configured).
 
* Sets-up a dedicated Python virtual environment for website.
 
* Sets-up a dedicated Python virtual environment for website. Python
 
  version can be specified (default is Python 2).
 
* Install ``futures`` package in Python virtual environment (required for
 
  Gunicorn in combination withg Python 2.7).
 
  Gunicorn in combination with Python 2.7).
 
* Install Gunicorn in Python virtual environment.
 
* Installs additional packages required for running the role in Python virtual
 
  environment (as configured).
 
* Configures systemd to run the website code (using Gunicorn)
 
* Deploys the HTTPS TLS private key and certificate (for website vhost).
 
* Configures nginx to serve the website (static files served directly, requests
 
@@ -1755,17 +1756,17 @@ Parameters
 

	
 
**fqdn** (string, mandatory)
 
  Fully-qualified domain name where the website is reachable. This value is used
 
  for calculating the user/group name for dedicated website user, as well as
 
  home directory of the website user (where data/code should be stored at).
 

	
 
**futures_version** (string, optional, ``3.1.1``)
 
**futures_version** (string, optional, ``3.2.0``)
 
  Version of ``futures`` package to deploy in virtual environment. Required by
 
  Gunicorn when using Python 2.7. Default version is tested with the test site.
 

	
 
**gunicorn_version** (string, optional, ``19.7.1``)
 
**gunicorn_version** (string, optional, ``19.9.0``)
 
  Version of Gunicorn to deploy in virtual environment for running the WSGI
 
  application. Default version is tested with the test site.
 

	
 
**https_tls_certificate** (string, optional, ``{{ lookup('file', tls_certificate_dir + '/' + fqdn + '_https.pem') }}``)
 
  X.509 certificate used for TLS for HTTPS service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ fqdn }}_https.pem``.
 
@@ -1783,12 +1784,15 @@ Parameters
 
  Additional headers to set when proxying request to Gunicorn. Keys are header
 
  names, values are header values. Both should be compatible with Nginx
 
  ``proxy_set_header``. If you need to provide an empty value, use quotes (don't
 
  forget to surround them by another set of quotes for YAML syntax, for example
 
  ``"\"\""`` or ``'""'``).
 

	
 
**python_version** (string, optional, ``2``)
 
  Python version to use when setting-up the Python virtual environment.
 

	
 
**rewrites** (list, optional, ``[]``)
 
  A list of rewrite rules that are applied to incoming requests. Each element of
 
  the list should be a string value compatible with the format of ``nginx``
 
  option ``rewrite``. The keyword ``rewrite`` itself should be omitted, as well
 
  as trailing semi-colon (``;``).
 

	
roles/wsgi_website/defaults/main.yml
Show inline comments
 
@@ -7,17 +7,19 @@ rewrites: []
 
static_locations: []
 
use_paste: false
 
virtualenv_packages: []
 
environment_variables: {}
 
https_tls_certificate: "{{ lookup('file', tls_certificate_dir + '/' + fqdn + '_https.pem') }}"
 
https_tls_key: "{{ lookup('file', tls_private_key_dir + '/' + fqdn + '_https.key') }}"
 
gunicorn_version: "19.7.1"
 
futures_version: "3.1.1"
 
gunicorn_version: "19.9.0"
 
futures_version: "3.2.0"
 
website_mail_recipients: "root"
 
environment_indicator: null
 
proxy_headers: {}
 
python_version: 2
 
wsgi_requirements: []
 

	
 
# Internal parameters.
 
admin: "admin-{{ fqdn | replace('.', '_') }}"
 
user: "web-{{ fqdn | replace('.', '_') }}"
 
home: "/var/www/{{ fqdn }}"
 
python_interpreter: "/usr/bin/python{{ python_version }}"
roles/wsgi_website/molecule/default/playbook.yml
Show inline comments
 
@@ -54,12 +54,13 @@
 
        - dnspython==1.15.0,<1.20.0
 
        - ptpython==0.41
 
        - prompt-toolkit==1.0.15
 
      website_mail_recipients: user
 
      wsgi_application: testapp:application
 
      wsgi_requirements: []
 
      python_version: 3
 

	
 
    - role: wsgi_website
 
      fqdn: parameters-paste-req
 
      use_paste: true
 
      virtualenv_packages:
 
        - click==6.7
roles/wsgi_website/molecule/default/tests/data/python/paste/testapp.py
Show inline comments
 
import os
 
import sys
 

	
 
import flask
 

	
 
from flask import Flask
 
app = Flask(__name__, static_url_path='/keep-default-static-out-of-way')
 

	
 

	
 
@@ -17,12 +19,13 @@ def index(path):
 
  </head>
 
  <body>
 
    <p>This is the WSGI application at {host}.</p>
 
    <p>Requested URL was: {scheme}://{host}{script}{path}
 
    <p>MY_ENV_VAR: {my_env_var}</p>
 
    <p>Accept-Encoding: {accept_encoding}</p>
 
    <p>Python version: {python_version}</p>
 
  </body>
 
</html>
 
"""
 

	
 
    environ = flask.request.environ
 

	
 
@@ -30,10 +33,11 @@ def index(path):
 
    parameters['host'] = environ['HTTP_HOST']
 
    parameters['scheme'] = environ['wsgi.url_scheme']
 
    parameters['script'] = environ['SCRIPT_NAME']
 
    parameters['path'] = environ['PATH_INFO']
 
    parameters['my_env_var'] = os.environ.get('MY_ENV_VAR', None)
 
    parameters['accept_encoding'] = environ.get('HTTP_ACCEPT_ENCODING')
 
    parameters['python_version'] = "%s.%s.%s" % (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
 

	
 
    output = template.format(**parameters)
 

	
 
    return output
roles/wsgi_website/molecule/default/tests/data/python/wsgi/testapp.py
Show inline comments
 
#!/usr/bin/env python
 

	
 
import os
 
import sys
 

	
 

	
 
def application(environ, start_response):
 
    status = '200 OK'
 

	
 
    template = """<!DOCTYPE html>
 
@@ -14,25 +15,27 @@ def application(environ, start_response):
 
  </head>
 
  <body>
 
    <p>This is the WSGI application at {host}.</p>
 
    <p>Requested URL was: {scheme}://{host}{script}{path}
 
    <p>MY_ENV_VAR: {my_env_var}</p>
 
    <p>Accept-Encoding: {accept_encoding}</p>
 
    <p>Python version: {python_version}</p>
 
  </body>
 
</html>
 
"""
 

	
 
    parameters = {}
 
    parameters['host'] = environ['HTTP_HOST']
 
    parameters['scheme'] = environ['wsgi.url_scheme']
 
    parameters['script'] = environ['SCRIPT_NAME']
 
    parameters['path'] = environ['PATH_INFO']
 
    parameters['my_env_var'] = os.environ.get('MY_ENV_VAR', None)
 
    parameters['accept_encoding'] = environ.get('HTTP_ACCEPT_ENCODING')
 
    parameters['python_version'] = "%s.%s.%s" % (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
 

	
 
    output = template.format(**parameters)
 

	
 
    response_headers = [('Content-type', 'text/html'),
 
                        ('Content-Length', str(len(output)))]
 
    start_response(status, response_headers)
 

	
 
    return [output]
 
    return [output.encode('utf-8')]
roles/wsgi_website/molecule/default/tests/test_default.py
Show inline comments
 
@@ -295,30 +295,28 @@ def test_python_virtualenv_wrapper_script(host, wrapper_script, expected_owner,
 
        assert command.rc == 0
 

	
 

	
 
@pytest.mark.parametrize("admin_user, pip_path, expected_packages",  [
 
    ('admin-parameters-mandatory', '/var/www/parameters-mandatory/virtualenv/bin/pip', [
 
        "argparse==1.2.1",
 
        "futures==3.1.1",
 
        "gunicorn==19.7.1",
 
        "futures==3.2.0",
 
        "gunicorn==19.9.0",
 
        "wsgiref==0.1.2"
 
    ]),
 
    ('admin-parameters-optional_local', '/var/www/parameters-optional.local/virtualenv/bin/pip', [
 
        "Pygments==2.2.0",
 
        "argparse==1.2.1",
 
        "dnspython==1.15.0",
 
        "docopt==0.6.2",
 
        "futures==3.1.0",
 
        "gunicorn==19.7.0",
 
        "jedi==0.12.1",
 
        "parso==0.3.1",
 
        "prompt-toolkit==1.0.15",
 
        "ptpython==0.41",
 
        "six==1.11.0",
 
        "wcwidth==0.1.7",
 
        "wsgiref==0.1.2"
 
    ]),
 
    ('admin-parameters-paste-req', '/var/www/parameters-paste-req/virtualenv/bin/pip', [
 
        "Flask==0.12.2",
 
        "Jinja2==2.9.6",
 
        "MarkupSafe==1.0",
 
        "Paste==2.0.3",
 
@@ -535,6 +533,31 @@ def test_website_enabled(host, config_file, expected_content):
 
    """
 

	
 
    config = host.file(config_file)
 

	
 
    assert config.is_symlink
 
    assert config.linked_to == expected_content
 

	
 

	
 
@pytest.mark.parametrize("python_path, expected_version_startswith", [
 
    ("/var/www/parameters-mandatory/virtualenv/bin/python", "Python 2"),
 
    ("/var/www/parameters-optional.local/virtualenv/bin/python", "Python 3"),
 
    ("/var/www/parameters-paste-req/virtualenv/bin/python", "Python 2"),
 
])
 
def test_python_version_in_virtualenv(host, python_path, expected_version_startswith):
 
    """
 
    Tests if correct Python version is used inside of virtual
 
    environment.
 
    """
 

	
 
    with host.sudo():
 

	
 
        python_version = host.run(python_path + " --version")
 

	
 
        # Python 2 outputs version to stderr, Python 3 outputs it to
 
        # stdout. Go figure.
 
        python_version_output = python_version.stdout + python_version.stderr
 

	
 
        assert python_version.rc == 0
 

	
 
        # Python binary prints out version to stderr.
 
        assert python_version_output.startswith(expected_version_startswith)
roles/wsgi_website/molecule/default/tests/test_parameters_mandatory.py
Show inline comments
 
@@ -39,12 +39,13 @@ def test_index_page(host):
 

	
 
    assert page.rc == 0
 
    assert "This is the WSGI application at parameters-mandatory." in page.stdout
 
    assert "Requested URL was: https://parameters-mandatory/" in page.stdout
 
    assert "MY_ENV_VAR: None" in page.stdout
 
    assert "Accept-Encoding: plain" in page.stdout
 
    assert "Python version: 2." in page.stdout
 

	
 

	
 
def test_static_file_serving(host):
 
    """
 
    Tests serving of static files.
 
    """
roles/wsgi_website/molecule/default/tests/test_parameters_optional.py
Show inline comments
 
@@ -60,12 +60,13 @@ def test_index_page(host):
 

	
 
    assert page.rc == 0
 
    assert "This is the WSGI application at parameters-optional.local." in page.stdout
 
    assert "Requested URL was: https://parameters-optional.local/" in page.stdout
 
    assert "MY_ENV_VAR: My environment variable" in page.stdout
 
    assert "Accept-Encoding: None" in page.stdout
 
    assert "Python version: 3." in page.stdout
 

	
 

	
 
def test_static_file_serving(host):
 
    """
 
    Tests serving of static files.
 
    """
roles/wsgi_website/molecule/default/tests/test_parameters_paste_req.py
Show inline comments
 
@@ -65,12 +65,13 @@ def test_index_page(host):
 

	
 
    assert page.rc == 0
 
    assert "This is the WSGI application at parameters-paste-req." in page.stdout
 
    assert "Requested URL was: https://parameters-paste-req/" in page.stdout
 
    assert "MY_ENV_VAR: None" in page.stdout
 
    assert "Accept-Encoding: plain" in page.stdout
 
    assert "Python version: 2." in page.stdout
 

	
 

	
 
def test_static_file_serving(host):
 
    """
 
    Tests serving of static files.
 
    """
roles/wsgi_website/tasks/main.yml
Show inline comments
 
@@ -91,13 +91,13 @@
 
    state: directory
 
    owner: "{{ admin }}"
 
    group: "{{ user }}"
 
    mode: 02750
 

	
 
- name: Create Python virtual environment
 
  command: '/usr/bin/virtualenv --prompt "({{ fqdn }})" "{{ home }}/virtualenv"'
 
  command: '/usr/bin/virtualenv --python "{{ python_interpreter }}" --prompt "({{ fqdn }})" "{{ home }}/virtualenv"'
 
  args:
 
    creates: "{{ home }}/virtualenv/bin/activate"
 
  become: true
 
  become_user: "{{ admin }}"
 
  tags:
 
    # [ANSIBLE0012] Commands should not change things if nothing needs doing
0 comments (0 inline, 0 general)