diff --git a/.gitignore b/.gitignore index de0ff73..2508438 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ debug.log* settings.ini deployment-key deployment-key.pub + +# Deployment +.vagrant/ +staging.yml diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..5b8bbfc --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,24 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure("2") do |config| + # Every Vagrant development environment requires a box. You can search for + # boxes at https://vagrantcloud.com/search. + config.vm.box = "centos/7" + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine and only allow access + # via 127.0.0.1 to disable public access + config.vm.network "forwarded_port", guest: 80, host: 8888, host_ip: "127.0.0.1" + config.vm.network "forwarded_port", guest: 443, host: 8889, host_ip: "127.0.0.1" + + # Provision VM using Ansible playbook + config.vm.provision "ansible" do |ansible| + ansible.verbose = "v" + ansible.playbook = "playbook.yml" + end +end diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..00f763d --- /dev/null +++ b/playbook.yml @@ -0,0 +1,20 @@ +--- +- hosts: all + become_user: root + become_method: sudo + become: yes + + pre_tasks: + - name: Check if running under Vagrant + stat: + path: /vagrant + register: vagrant_dir + + roles: + - database + - webserver + + vars: + ansible_python_interpreter: python2 + db_user: 'breccia' + db_pass: 'breccia' diff --git a/roles/database/tasks/main.yml b/roles/database/tasks/main.yml new file mode 100644 index 0000000..24c5fff --- /dev/null +++ b/roles/database/tasks/main.yml @@ -0,0 +1,36 @@ +--- +- name: Update system packages + yum: + name: '*' + state: latest + +- name: Install system prerequisites + yum: + name: '{{ packages }}' + state: latest + vars: + packages: + - mariadb + - mariadb-devel + - mariadb-server + - python + # For Ansible - not used at runtime + - MySQL-python + +- name: Restart database server + service: + name: mariadb + state: restarted + enabled: yes + +- name: Create database + mysql_db: + name: '{{ db_name }}' + state: present + +- name: Create database user + mysql_user: + name: '{{ db_user }}' + password: '{{ db_pass }}' + state: present + priv: '{{ db_name }}.*:ALL' diff --git a/roles/webserver/defaults/main.yml b/roles/webserver/defaults/main.yml new file mode 100644 index 0000000..b016ee1 --- /dev/null +++ b/roles/webserver/defaults/main.yml @@ -0,0 +1,16 @@ +--- +deploy_mode_dict: + 1: Production + 2: Staging + 3: Development +deploy_mode: 3 + +secret_key: '{{ lookup("password", "/tmp/secretkeyfile") }}' + +project_name: 'breccia-mapper' +project_full_name: 'breccia_mapper' +project_dir: '/var/www/{{ project_name }}' +venv_dir: '{{ project_dir }}/venv' +web_user: nginx +web_group: nginx +db_name: '{{ project_name }}' \ No newline at end of file diff --git a/roles/webserver/tasks/main.yml b/roles/webserver/tasks/main.yml new file mode 100644 index 0000000..b1d7bdb --- /dev/null +++ b/roles/webserver/tasks/main.yml @@ -0,0 +1,221 @@ +--- +- name: Test connection + ping: + +- name: Enable EPEL + yum: + name: epel-release + state: latest + +- name: Update system packages + yum: + name: '*' + state: latest + +- name: Install system prerequisites + yum: + name: '{{ packages }}' + state: latest + vars: + packages: + - gcc + - git + - nginx + - python36 + - python36-devel + - python36-pip + - python36-setuptools + - python36-virtualenv + - policycoreutils-python + - python + - python-setuptools + - python2-cryptography + +- name: (Vagrant only) Clone / update from local repo + git: + repo: '/vagrant' + dest: '{{ project_dir }}' + when: vagrant_dir.stat.exists == True + +- name: (Vagrant only) Copy local settings file + copy: + src: '{{ settings_file | default("settings.ini") }}' + dest: '{{ project_dir }}/settings.ini' + owner: '{{ web_user }}' + group: '{{ web_group }}' + mode: 0600 + when: vagrant_dir.stat.exists == True + +- name: (Vagrant only) Add DB to settings file + ini_file: + path: '{{ project_dir }}/settings.ini' + section: settings + option: DATABASE_URL + value: 'mysql://{{ db_user }}:{{ db_pass }}@localhost:3306/{{ db_name }}' + when: vagrant_dir.stat.exists == True + +- name: Copy deploy key + copy: + src: 'deployment-key' + dest: '/tmp/deployment-key' + mode: 0600 + when: vagrant_dir.stat.exists == False + +- name: Clone / update from source repo + git: + repo: 'git@github.com:Southampton-RSG/breccia-mapper.git' + dest: '{{ project_dir }}' + key_file: '/tmp/deployment-key' + version: '{{ branch | default ("master") }}' + accept_hostkey: yes + when: vagrant_dir.stat.exists == False + +- name: Copy and populate settings template + template: + src: 'settings.j2' + dest: '{{ project_dir }}/settings.ini' + owner: '{{ web_user }}' + group: '{{ web_group }}' + mode: 0600 + when: vagrant_dir.stat.exists == False + +- name: Set ownership of source directory + file: + path: '{{ project_dir }}' + owner: '{{ web_user }}' + group: '{{ web_group }}' + recurse: yes + +- name: Install pip requirements + pip: + requirements: '{{ project_dir }}/requirements.txt' + virtualenv: '{{ venv_dir }}' + virtualenv_command: virtualenv-3 + +- name: Create static directory + file: + path: '{{ project_dir }}/static' + state: directory + owner: '{{ web_user }}' + group: '{{ web_group }}' + mode: 0755 + +- name: Run Django setup stages + django_manage: + command: '{{ item }}' + app_path: '{{ project_dir }}' + virtualenv: '{{ venv_dir }}' + with_items: + - migrate + - collectstatic + +- name: Apply SELinux type + file: + path: '{{ project_dir }}/static' + state: directory + setype: httpd_sys_content_t + +- name: (Not production) Set SELinux permissive mode + selinux_permissive: + name: httpd_t + permissive: yes + when: deploy_mode > 1 + +- name: Install uWSGI + pip: + name: uwsgi + state: latest + executable: pip3 + +- name: Setup uWSGI config + file: + path: /etc/uwsgi/sites + state: directory + mode: 0755 + +- name: Setup uWSGI service + template: + src: uwsgi-service.j2 + dest: /etc/systemd/system/uwsgi.service + +- name: Ensure uWSGI running + service: + name: uwsgi + state: started + enabled: yes + +- name: Copy web config files + template: + src: uwsgi-site.j2 + dest: '/etc/uwsgi/sites/{{ project_name }}.ini' + +- name: Generate self-signed SSL certificate + block: + - name: Create directories + file: + path: "{{ item }}" + state: directory + with_items: + - /etc/ssl + - /etc/ssl/crt + - /etc/ssl/private + - /etc/ssl/csr + + - name: Create keys + openssl_privatekey: + path: /etc/ssl/private/{{ inventory_hostname }}.pem + owner: '{{ web_user }}' + group: '{{ web_user }}' + + - name: Create Certificate Signing Request (CSR) + openssl_csr: + path: /etc/ssl/csr/{{ inventory_hostname }}.csr + privatekey_path: /etc/ssl/private/{{ inventory_hostname }}.pem + common_name: "{{ inventory_hostname }}" + owner: '{{ web_user }}' + group: '{{ web_user }}' + + - name: Generate certificate + openssl_certificate: + path: /etc/ssl/crt/{{ inventory_hostname }}.crt + privatekey_path: /etc/ssl/private/{{ inventory_hostname }}.pem + csr_path: /etc/ssl/csr/{{ inventory_hostname }}.csr + provider: selfsigned + owner: '{{ web_user }}' + group: '{{ web_user }}' + + - name: Copy Nginx site + template: + src: nginx-site-ssl.j2 + dest: '/etc/nginx/conf.d/{{ project_name }}-ssl.conf' + owner: '{{ web_user }}' + group: '{{ web_group }}' + + when: deploy_mode > 1 + +- name: Copy Nginx site + template: + src: nginx-site.j2 + dest: '/etc/nginx/conf.d/{{ project_name }}.conf' + owner: '{{ web_user }}' + group: '{{ web_group }}' + +- name: Restart uWSGI and Nginx + service: + name: "{{ item }}" + state: restarted + enabled: yes + with_items: + - uwsgi + - nginx + +- name: Open webserver ports on firewall + firewalld: + service: '{{ item }}' + state: enabled + permanent: yes + immediate: yes + loop: + - http + - https + when: vagrant_dir.stat.exists == False diff --git a/roles/webserver/templates/nginx-site-ssl.j2 b/roles/webserver/templates/nginx-site-ssl.j2 new file mode 100644 index 0000000..ba5f461 --- /dev/null +++ b/roles/webserver/templates/nginx-site-ssl.j2 @@ -0,0 +1,27 @@ +server { + # HTTP/2 allows requests to be pipelined within a single connection + listen 443 ssl http2; + server_name {{ inventory_hostname }} localhost 127.0.0.1; + + ssl_certificate /etc/ssl/crt/{{ inventory_hostname }}.crt; + ssl_certificate_key /etc/ssl/private/{{ inventory_hostname }}.pem; + + # Cache and tickets improve performance by ~10% on small requests + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 4h; + ssl_session_tickets on; + + location /favicon.ico { + alias {{ project_dir }}/static/img/favicon.ico; + } + + location /static/ { + alias {{ project_dir }}/static/; + } + + location / { + include uwsgi_params; + uwsgi_pass unix:/run/uwsgi/{{ project_name }}.sock; + uwsgi_buffers 256 16k; + } +} \ No newline at end of file diff --git a/roles/webserver/templates/nginx-site.j2 b/roles/webserver/templates/nginx-site.j2 new file mode 100644 index 0000000..f9f85c1 --- /dev/null +++ b/roles/webserver/templates/nginx-site.j2 @@ -0,0 +1,17 @@ +server { + listen 80; + server_name {{ inventory_hostname }} localhost 127.0.0.1; + + location /favicon.ico { + alias {{ project_dir }}/static/img/favicon.ico; + } + + location /static/ { + alias {{ project_dir }}/static/; + } + + location / { + include uwsgi_params; + uwsgi_pass unix:/run/uwsgi/{{ project_name }}.sock; + } +} \ No newline at end of file diff --git a/roles/webserver/templates/settings.j2 b/roles/webserver/templates/settings.j2 new file mode 100644 index 0000000..4eea8af --- /dev/null +++ b/roles/webserver/templates/settings.j2 @@ -0,0 +1,18 @@ +# Template populated on {{ template_run_date }} +[settings] + +SECRET_KEY={{ secret_key }} +DEBUG={{ "True" if deploy_mode > 1 else "False" }} +ALLOWED_HOSTS={{ inventory_hostname }},localhost,127.0.0.1 +DATABASE_URL=mysql://{{ db_user }}:{{ db_pass }}@localhost:3306/{{ db_name }} + +# LDAP auth +AUTH_LDAP_SERVER_URI={{ ldap_server }} +AUTH_LDAP_USER_SEARCH={{ ldap_user_search }} +AUTH_LDAP_GROUP_SEARCH={{ ldap_group_search }} + +# PURE settings +PURE_URL={{ pure_api_url }} +PURE_API_KEY={{ pure_api_key }} +PURE_USERNAME={{ pure_api_user }} +PURE_PASSWORD={{ pure_api_pass }} diff --git a/roles/webserver/templates/uwsgi-service.j2 b/roles/webserver/templates/uwsgi-service.j2 new file mode 100644 index 0000000..fffb99c --- /dev/null +++ b/roles/webserver/templates/uwsgi-service.j2 @@ -0,0 +1,13 @@ +[Unit] +Description=uWSGI Emperor Service + +[Service] +ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown {{ web_user }}:{{ web_group }} /run/uwsgi' +ExecStart=/usr/local/bin/uwsgi --emperor /etc/uwsgi/sites +Restart=always +KillSignal=SIGQUIT +Type=notify +NotifyAccess=all + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/webserver/templates/uwsgi-site.j2 b/roles/webserver/templates/uwsgi-site.j2 new file mode 100644 index 0000000..b0f83c0 --- /dev/null +++ b/roles/webserver/templates/uwsgi-site.j2 @@ -0,0 +1,18 @@ +[uwsgi] +project = {{ project_name }} +uid = {{ web_user }} +base = /var/www + +chdir = %(base)/%(project) +home = {{ venv_dir }} +module = {{ project_full_name }}.wsgi:application +logto = %(chdir)/%(project).log + +master = true +processes = 2 +listen = 128 + +socket = /run/uwsgi/%(project).sock +chown-socket = %(uid):{{ web_group }} +chmod-socket = 660 +vacuum = true