diff --git a/.gitignore b/.gitignore index 6500dff..5234daa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__/ /calsocial/translations/*/LC_MESSAGES/*.mo /.pytest_cache/ /.env +/.vagrant/ +/ansible/*.retry diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..d691502 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,74 @@ +# -*- 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| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://vagrantcloud.com/search. + config.vm.box = 'fedora/28-cloud-base' + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + # NOTE: This will enable public access to the opened port + config.vm.network "forwarded_port", guest: 80, host: 8080 + + # 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: 8080, host_ip: "127.0.0.1" + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + config.vm.synced_folder './', '/vagrant', type: 'sshfs' + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + config.vm.provision "ansible_local" do |ansible| + ansible.compatibility_mode = '2.0' + ansible.install = true + ansible.provisioning_path = '/vagrant/ansible' + ansible.playbook = 'dev.yml' + end +end diff --git a/ansible/dev.yml b/ansible/dev.yml new file mode 100644 index 0000000..f84b1f6 --- /dev/null +++ b/ansible/dev.yml @@ -0,0 +1,23 @@ +--- + +- name: Configuration for local development on Vagrant + hosts: all + become: yes + vars: + user_name: vagrant + group_name: vagrant + + roles: + - common + - python + - role: gunicorn + autostart: false + enabled: false + - role: nginx + use_ssl: false + enabled: false + + tasks: + - name: Allow virtualenv python to bind to port 80 + command: setcap cap_net_bind_service=ep /usr/bin/python3.6 + changed_when: false diff --git a/ansible/install.sh b/ansible/install.sh new file mode 100644 index 0000000..2acd7fa --- /dev/null +++ b/ansible/install.sh @@ -0,0 +1,12 @@ +#! /usr/bin/env bash + +if [ ! -f /etc/ansible/hosts ] +then + echo "Installing Ansible..." + sudo dnf remove ansible + sudo dnf install ansible-python3 + + printf 'localhost\n' | sudo tee /etc/ansible/hosts > /dev/null +fi + +echo "Ansible is installed." diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000..289c1de --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,8 @@ +--- + +- name: Install required packages + dnf: + name: "{{ item }}" + state: present + with_items: + - libselinux-python diff --git a/ansible/roles/common/vars/main.yml b/ansible/roles/common/vars/main.yml new file mode 100644 index 0000000..c8be33d --- /dev/null +++ b/ansible/roles/common/vars/main.yml @@ -0,0 +1,10 @@ +--- + +# Project name +project_name: calendar.social + +# Project path +project_path: /vagrant + +# Flask app path +application_path: /vagrant/app diff --git a/ansible/roles/gunicorn/tasks/main.yml b/ansible/roles/gunicorn/tasks/main.yml new file mode 100644 index 0000000..9465521 --- /dev/null +++ b/ansible/roles/gunicorn/tasks/main.yml @@ -0,0 +1,56 @@ +--- + +- name: Install Supervisor + dnf: + name: "{{ item }}" + state: present + with_items: + - supervisor + +- name: Start supervisord + service: + name: supervisord + state: restarted + +- name: Create the Gunicorn config directory + file: + path: /etc/gunicorn + state: directory + owner: "{{ user_name }}" + group: "{{ group_name }}" + mode: 0700 + +- name: Create the Gunicorn config file in /etc/gunicorn + template: + src: gunicorn.conf.j2 + dest: /etc/gunicorn/gunicorn.conf + +- name: Create the Gunicorn log directory + file: + path: /var/log/gunicorn + state: directory + owner: "{{ user_name }}" + group: "{{ group_name }}" + mode: 0700 + +- name: Create the Supervisor config file for Gunicorn + template: + src: supervisor.conf.j2 + dest: /etc/supervisord.d/gunicorn.ini + +- name: Re-read the Supervisor config files + supervisorctl: + name: gunicorn + state: present + +- name: Start Gunicorn with supervisord + supervisorctl: + name: gunicorn + state: restarted + when: enabled + +- name: Stop Gunicorn for local dev + supervisorctl: + name: gunicorn + state: stopped + when: not enabled diff --git a/ansible/roles/gunicorn/templates/gunicorn.conf.j2 b/ansible/roles/gunicorn/templates/gunicorn.conf.j2 new file mode 100644 index 0000000..89df143 --- /dev/null +++ b/ansible/roles/gunicorn/templates/gunicorn.conf.j2 @@ -0,0 +1,9 @@ +import multiprocessing + +workers = multiprocessing.cpu_count() * 2 + 1 +proc_name = 'gunicorn' +bind = '127.0.0.1:8000' +errorlog = '/var/log/gunicorn/gunicorn-error.log' +accesslog = '/var/log/gunicorn/gunicorn-access.log' +loglevel = 'warning' +timeout = 60 diff --git a/ansible/roles/gunicorn/templates/supervisor.conf.j2 b/ansible/roles/gunicorn/templates/supervisor.conf.j2 new file mode 100644 index 0000000..cc61cad --- /dev/null +++ b/ansible/roles/gunicorn/templates/supervisor.conf.j2 @@ -0,0 +1,8 @@ +[program:gunicorn] +command=pipenv run gunicorn wsgi:app -c /etc/gunicorn/gunicorn.conf --pythonpath {{ application_path }} +directory={{ application_path }} +user={{ user_name }} +group={{ group_name }} +autorestart=true +autostart={{ autostart | bool | lower }} +redirect_stderr=true \ No newline at end of file diff --git a/ansible/roles/nginx/handlers/main.yml b/ansible/roles/nginx/handlers/main.yml new file mode 100644 index 0000000..0cb7296 --- /dev/null +++ b/ansible/roles/nginx/handlers/main.yml @@ -0,0 +1,11 @@ +--- + +- name: Reload Nginx + service: + name: nginx + state: reloaded + +- name: Stop Nginx + service: + name: nginx + state: stopped diff --git a/ansible/roles/nginx/tasks/main.yml b/ansible/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..0c7db43 --- /dev/null +++ b/ansible/roles/nginx/tasks/main.yml @@ -0,0 +1,42 @@ +--- + +- name: Install Nginx + dnf: + name: "{{ item }}" + state: present + with_items: + - nginx + +- name: Create the Nginx configuration file for SSL + template: + src: site-ssl.conf.j2 + dest: /etc/nginx/conf.d/{{ project_name }}-ssl.conf + when: use_ssl + notify: Reload Nginx + +- name: Create the Nginx configuration file (non-SSL) + template: + src: site.conf.j2 + dest: /etc/nginx/conf.d/{{ project_name }}.conf + when: not use_ssl + notify: Reload Nginx + +- name: Ensure that the default site is removed + file: + path: /etc/nginx/conf.d/default.conf + state: absent + +- name: Ensure Nginx service is started, enable service on restart + service: + name: nginx + state: restarted + enabled: yes + when: enabled + +- name: Stop nginx for local dev, disable service + service: + name: nginx + state: stopped + enabled: no + notify: Stop Nginx + when: not enabled diff --git a/ansible/roles/nginx/templates/site-ssl.conf.j2 b/ansible/roles/nginx/templates/site-ssl.conf.j2 new file mode 100644 index 0000000..fdebd41 --- /dev/null +++ b/ansible/roles/nginx/templates/site-ssl.conf.j2 @@ -0,0 +1,41 @@ +upstream appserver { + server localhost:8000 fail_timeout=0; +} + +server { + listen 80; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl deferred; + server_name {{ host_name }}; + + ssl_certificate {{ home_path }}/{{ project_name }}.crt; + ssl_certificate_key {{ home_path }}/{{ project_name }}.key; + ssl_session_cache shared:SSL:32m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + access_log /var/log/nginx/{{ project_name }}.access.log; + error_log /var/log/nginx/{{ project_name }}.error.log info; + + keepalive_timeout 5; + + location /static { + alias {{ project_path }}/static; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_read_timeout 180s; + + if (!-f $request_filename) { + proxy_pass http://appserver; + break; + } + } +} diff --git a/ansible/roles/nginx/templates/site.conf.j2 b/ansible/roles/nginx/templates/site.conf.j2 new file mode 100644 index 0000000..09dd249 --- /dev/null +++ b/ansible/roles/nginx/templates/site.conf.j2 @@ -0,0 +1,29 @@ +upstream appserver { + server localhost:8000 fail_timeout=0; +} + +server { + listen 80; + server_name {{ host_name }}; + + access_log /var/log/nginx/{{ project_name }}.access.log; + error_log /var/log/nginx/{{ project_name }}.error.log info; + + keepalive_timeout 5; + + location /static { + alias {{ project_path }}/static; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_read_timeout 180s; + + if (-f $request_filename) { + proxy_pass http://appserver; + break; + } + } +} diff --git a/ansible/roles/nginx/vars/main.yml b/ansible/roles/nginx/vars/main.yml new file mode 100644 index 0000000..3fd5904 --- /dev/null +++ b/ansible/roles/nginx/vars/main.yml @@ -0,0 +1,3 @@ +--- + +host_name: calendar-social.local diff --git a/ansible/roles/python/tasks/main.yml b/ansible/roles/python/tasks/main.yml new file mode 100644 index 0000000..689234e --- /dev/null +++ b/ansible/roles/python/tasks/main.yml @@ -0,0 +1,22 @@ +--- + +- name: Install common python packages + dnf: + name: "{{ item }}" + state: present + with_items: + - pipenv + +- name: Delete Python cache files + command: find . -type d -name __pycache__ -exec rm -r {} + + args: + chdir: "{{ project_path }}" + changed_when: false + +- name: Install packages + command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy + +- name: Install development related packages + command: pipenv install --python=/usr/bin/python3.6m --three --system --deploy --dev + args: + chdir: "{{ project_path }}" diff --git a/calsocial/__main__.py b/calsocial/__main__.py index 26c561c..8e3818c 100644 --- a/calsocial/__main__.py +++ b/calsocial/__main__.py @@ -6,4 +6,4 @@ from calsocial import CalendarSocialApp app = CalendarSocialApp('calsocial') -app.run() +app.run(host='0.0.0.0', port=80)