diff --git a/.gitignore b/.gitignore index 25087f1..42307a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,61 @@ host_vars/* public_keys/* -!host_vars/UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de.example -uberspaces +/*.yml +!/uberspaces.example +!/uberspaces.yml.example +!/site.yml.example + + + + +# venv & app stuff + +bin/ +lib64 +pyvenv.cfg + +cache/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# pyenv +.python-version + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/README.md b/README.md index 7624846..1c25549 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,10 @@ This playbook helps you set up and manage your Uberspace(s). -It configures a few common things that I find essential for Uberspaces and it is extensible for other stuff. - ## Current features -- [Let's Encrypt SSL certificates](https://wiki.uberspace.de/webserver:https#let_s-encrypt-zertifikate) -- [WordPress](https://wordpress.org/) using the awesome [Bedrock](https://roots.io/bedrock/) boilerplate -- [Ruby on Rails](http://rubyonrails.org/) apps +- Registering domains for web and mail, and optionally symlinking docroot to the home directory +- [WordPress](https://wordpress.org/) using the [Bedrock](https://roots.io/bedrock/) boilerplate ## Requirements @@ -18,24 +15,9 @@ It configures a few common things that I find essential for Uberspaces and it is ## Usage 1. Copy `uberspaces.example` to `uberspaces` and add your Uberspace host(s) and username(s) -2. Copy `host_vars/UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de.example` to a new file named without the `.example` suffix and replace `UBERSPACE_NAME` with your username, e.g. `julia` and `UBERSPACE_HOST` with your Uberspace host, e.g. `eridanus`. -3. Add the domains you'd like to run on the respective Uberspace to the file created in step 2. -4. Repeat steps 2 and 3 for all your Uberspaces. -5. Run the playbook using `ansible-playbook --ask-pass site.yml`. -6. Enjoy! - -If you have an SSH keypair and your public key is installed in `~/.ssh/id_rsa.pub` on your local computer, the key will be stored in `~/.ssh/authorized_keys` on your Uberspace and you won't need the `--ask-pass` argument in subsequent runs. - -### Let's Encrypt - -Nothing to do or configure here. This works automagically for all your domains. - -### WordPress - -1. To set up a WordPress instance, simply create an entry under `wordpress_instances` in your `host_vars` file (see `host_vars/UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de.example` for an example) -2. Use the default `bedrock_repo` from `https://github.com/yeah/bedrock.git` or use your own forked repo of the boilerplate (Your Uberspace's public keys will be conveniently downloaded for you to `public_keys/` so you can use them as deploy keys for your private Git repos.) -3. Add the domains through which your WordPress should be accessible -4. Make sure to add these domains to the top-level `domains` section in the `host_vars` file as well! +2. Copy `site.yml.example` to `site.yml` or any other playbook name you want and start adapting it to your needs +3. The `uberspace` role handles uploading local ssh keys, generating a key for the managed machine and registering domains for web or mail against the uberspace host with an optional symlink to home +4. The `uberspace_wordpress_bedrock` role installs wordpress bedrock, creating the database for it and linking the docroot of the instance to the specified entry points #### Bonus: Deploy hooks @@ -58,88 +40,6 @@ curl -s 'https://julia.eridanus.uberspace.de/cgi-bin/wordpress-update-example_bl Or if you use [Planio](https://plan.io), simply enter your URL via **Settings** → **Repositories** → *your repo* → **Edit** → **Post-Receive webhook URL** -### Ruby on Rails apps - -Setting up and deploying your Ruby on Rails apps involves a little bit more work, but it's definitely worth it. Where else do you get such awesome Rails hosting for the price? Let's get started: - -#### Configure your playbook - -1. To set up a Rails app, create an entry under `rails_apps` in your `host_vars` file (see `host_vars/UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de.example` for an example) -2. Make sure to give it a `name` which should be only characters, numbers and maybe the underscore character – no spaces! -3. Add the domains through which your Rails app should be accessible -4. Make sure to add these domains to the top-level `domains` section in the `host_vars` file as well! -5. For `secret_key_base` generate a secret using `rake secret` in your Rails app -6. For `port` choose an unused port on your Uberspace between 32000 and 65000 - -That's it for Ansible. You can now run your playbook using `ansible-playbook site.yml`. - -#### Configure your Rails app - -To actually deploy your app, we're going to use [Capistrano](http://capistranorb.com/). Git clone your Rails app on your local computer and perform the following modifications: - -##### Modify your `Gemfile` - -1. Add or uncomment `gem 'capistrano-rails', group: :development` -2. Run `bundle install` and then `bundle exec cap install` - -##### Mofify your `Capfile` - -1. Add or uncomment `require 'capistrano/bundler'` -2. Add or uncomment `require 'capistrano/rails/migrations'` if your app is using MySQL - -Your `Capfile` should now look similar to this: - -```ruby -require "capistrano/setup" -require "capistrano/deploy" -require 'capistrano/bundler' -require 'capistrano/rails/migrations' - -Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r } -``` - -###### Modify your `config/deploy.rb` - -1. Find the line `set :application, 'my_app_name'` and replace `my_app_name` with the *exact same* name you chose earlier for the `rails_apps` entry in your `host_vars` file. -2. Find the line `set :repo_url, 'git@example.com:me/my_repo.git'` and add your Rails app's repo URL. If your repo is private, please add the keys you find in `public_keys/` within your playbook as deploy keys to your repository hoster. -3. Find the line `# set :deploy_to, '/var/www/my_app_name'`, uncomment it and set the value to `"~/www/rails/#{fetch :application}"` (notice the double quotes!) -4. Add the line `set :linked_files, fetch(:linked_files, []).push('config/database.yml')` -5. Add the line `after :publishing, :restart { execute :svc, "-du ~/service/rails-app-#{fetch :application}" }` within the `namespace :deploy` block - -Your `config/deploy.rb` should now look similar to this: - -```ruby -lock '3.5.0' - -set :application, 'example_app' -set :repo_url, 'git@example.plan.io:example-app.git' -set :deploy_to, "~/www/rails/#{fetch :application}" -set :linked_files, fetch(:linked_files, []).push('config/database.yml') - -namespace :deploy do - after :publishing, :restart { execute :svc, "-du ~/service/rails-app-#{fetch :application}" } -end -``` - - -##### Modify your `config/deploy/production.rb` - -1. Find and uncomment the line `# server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value` and replace `example.com` with the hostname of your Uberspace, e.g. `eridanus.uberspace.de` and `deploy` with your Uberspace username. - -Your `config/deploy/production.rb` should now look similar to this: - -```ruby -server 'eridanus.uberspace.de', user: 'julia', roles: %w{app db web} -``` - -That's it. You should be able to deploy your app using `bundle exec cap production deploy`. After some time, your Rails app should be humming nicely on your configured domain. - -### Cleanup - -As the Uberspace Playbook is still in development, it would make sense for you to run the cleanup tasks after every update from this repo. The cleanup tasks remove any files/configurations on your Uberspace which previous versions of the playbook may have installed but which are no longer needed. You can run the cleanup tasks like so: - -`ansible-playbook cleanup.yml` - ## License MIT. @@ -150,4 +50,6 @@ To contribute something you usually configure on your Uberspace, please fork thi ## Credits -[I](http://jan.sh) built this. By myself. On my computer. +[Jan](http://jan.sh) built this. By himself. On his computer. + +Then U7 came along with breaking changes and Peter Nerlich eventually tried to rewrite this for the new version. Though in the process, he self righteously changed stuff he deemed weird and removed some features he didn't use himself, completely destroying any backwards compatibility. diff --git a/common.yml b/common.yml deleted file mode 100644 index 2489137..0000000 --- a/common.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -- hosts: all - roles: - - role: common - - role: wordpress - when: wordpress_instances is defined - - role: rails - when: rails_apps is defined diff --git a/host_vars/UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de.example b/host_vars/UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de.example deleted file mode 100644 index 73942a1..0000000 --- a/host_vars/UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de.example +++ /dev/null @@ -1,31 +0,0 @@ ---- -domains: - - example.com - - www.example.com - - blog.example.com - -wordpress_instances: - - name: example_blog - domains: - - blog.example.com - - # Fork this repo and adapt to your requirements. Make sure to add your - # Uberspace's public SSH key as a "deploy key" to your Git repo. The - # playbook will download your Uberspace's public keys to the public_keys/ - # dir for you. - bedrock_repo: https://github.com/yeah/bedrock.git - - # For deployments via webhooks. Your webhook URL will be: - # https://UBERSPACE_NAME.UBERSPACE_HOST.uberspace.de/cgi-bin/wordpress-update-example_blog.cgi?secretsauce123 - webhook_key: secretsauce123 - -rails_apps: - - name: example_app - domains: - - myapp.example.com - - # generate a secure random string for this, e.g. by running `rake secret` in your app - secret_key_base: 0123456789abcdef - - # choose a random free port on your Uberspace, between 32000 and 65000 - port: 54321 diff --git a/roles/common/tasks/common.yml b/roles/common/tasks/common.yml deleted file mode 100644 index 08cd871..0000000 --- a/roles/common/tasks/common.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -- name: Upload local user's public key for SSH - authorized_key: - user: "{{ansible_user}}" - key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" - -- name: Generate SSH key pair for remote user on Uberspace - user: name={{ ansible_user }} generate_ssh_key=yes ssh_key_bits=4096 ssh_key_file=.ssh/id_rsa - -- name: Fetch remote user's public key (e.g. for Git access) - fetch: src=~/.ssh/id_rsa.pub dest=public_keys/{{ ansible_host }} flat=yes - -- name: Configure Git - template: - src: templates/gitconfig.j2 - dest: ~/.gitconfig - -- name: Run uberspace-add-domain - shell: uberspace-add-domain -w -d {{item}} - with_items: '{{ domains }}' - ignore_errors: yes - -- name: Enable Strict Transport Security - shell: uberspace-configure-webserver enable hsts - register: result - changed_when: "'new configuration will be live' in result.stdout" - -- name: Enable daemontools - shell: uberspace-setup-svscan - args: - creates: ~/service - -- name: Create ~/www symlink - file: - path: ~/www - src: /var/www/virtual/{{ ansible_user }}/ - state: link diff --git a/roles/common/tasks/letsencrypt.yml b/roles/common/tasks/letsencrypt.yml deleted file mode 100644 index 43a26ca..0000000 --- a/roles/common/tasks/letsencrypt.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -- name: Run uberspace-letsencrypt - shell: uberspace-letsencrypt creates=~/.config/letsencrypt/cli.ini - -- name: Add domains to ~/.config/letsencrypt/cli.ini - lineinfile: - dest: ~/.config/letsencrypt/cli.ini - line: domains = {{ domains | join(',') }} - regexp: ^domains\s*= - -- name: Add agree-tos and renew-by-default to ~/.config/letsencrypt/cli.ini - lineinfile: - dest: ~/.config/letsencrypt/cli.ini - line: "{{item}}" - with_items: - - "agree-tos = True" - - "renew-by-default = True" - -- name: Create script ~/bin/letsencrypt-renew - template: - src : templates/letsencrypt-renew.j2 - dest: ~/bin/letsencrypt-renew - mode: ugo+x - -- name: Create cron job for ~/bin/letsencrypt-renew - cron: - name: letsencrypt-renew - job: ~/bin/letsencrypt-renew - special_time: daily - -- name: Run ~/bin/letsencrypt-renew for the first time - shell: ~/bin/letsencrypt-renew - register: result - changed_when: "'Done renewing certificates.' in result.stdout" diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml deleted file mode 100644 index 6ed7ed9..0000000 --- a/roles/common/tasks/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -- include: common.yml -- include: letsencrypt.yml - when: domains is defined diff --git a/roles/common/templates/letsencrypt-renew.j2 b/roles/common/templates/letsencrypt-renew.j2 deleted file mode 100644 index e6346fe..0000000 --- a/roles/common/templates/letsencrypt-renew.j2 +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# {{ ansible_managed }} - -PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin - -openssl x509 -checkend 1728000 -in ~/.config/letsencrypt/live/{{ domains[0] }}/cert.pem > /dev/null - -if [ $? != 0 ]; then - # run let's encrypt - letsencrypt certonly - # import certificate - uberspace-add-certificate \ - -k ~/.config/letsencrypt/live/{{ domains[0] }}/privkey.pem \ - -c ~/.config/letsencrypt/live/{{ domains[0] }}/cert.pem - echo 'Done renewing certificates.' -fi diff --git a/roles/rails/tasks/main.yml b/roles/rails/tasks/main.yml deleted file mode 100644 index 8a85ac6..0000000 --- a/roles/rails/tasks/main.yml +++ /dev/null @@ -1,70 +0,0 @@ ---- -- name: Create directories for capistrano deployment - file: - dest: '~/www/rails/{{ item.name }}/shared/config' - state: directory - with_items: '{{ rails_apps }}' - -- name: Create MySQL database(s) - mysql_db: - name: "{{ ansible_user }}_{{ item.name }}" - collation: utf8_unicode_ci - encoding: utf8 - state: present - with_items: '{{ rails_apps }}' - -- name: Read MySQL database password from ~/.my.cnf - shell: sed -n -e '/\[client\]/,$p' ~/.my.cnf | grep password | head -1 | cut -d= -f2 | cut -d " " -f1 warn=no - register: database_password - changed_when: False - -- name: Create database.yml - template: - src : templates/database.yml.j2 - dest: '~/www/rails/{{ item.name }}/shared/config/database.yml' - with_items: '{{ rails_apps }}' - -- name: Create daemontools jobs for unicorns - shell: 'uberspace-setup-service rails-app-{{ item.name }} bundle exec unicorn --host 127.0.0.1 --port {{ item.port }}' - args: - creates: '~/etc/run-rails-app-{{ item.name }}/run' - with_items: '{{ rails_apps }}' - -- name: Configure daemontools jobs for rails - blockinfile: - args: - dest: '~/etc/run-rails-app-{{ item.name }}/run' - insertbefore: '^exec' - block: | - export RAILS_ENV=production - export RAILS_SERVE_STATIC_FILES=true - export SECRET_KEY_BASE={{ item.secret_key_base }} - cd ~/www/rails/{{ item.name }}/current - with_items: '{{ rails_apps }}' - -- name: Create web directory for rails apps - file: - path: '/var/www/virtual/{{ ansible_user }}/rails/{{ item.name }}/web' - state: directory - with_items: '{{ rails_apps }}' - -- name: Set up .htaccess for Rails apps and redirect all traffic to main domain - blockinfile: - dest: '/var/www/virtual/{{ ansible_user }}/rails/{{ item.name }}/web/.htaccess' - create: yes - insertbefore: BOF - block: | - RewriteEngine On - RewriteCond %{HTTP_HOST} !={{ item.domains[0] }} - RewriteRule (.*) https://{{ item.domains[0] }}/$1 [R=301,L] - RewriteRule (.*) http://localhost:{{ item.port }}/$1 [P] - with_items: '{{ rails_apps }}' - -- name: Create domain symlinks for rails apps - file: - dest: /var/www/virtual/{{ ansible_user }}/{{ item.1 }} - src: /var/www/virtual/{{ ansible_user }}/rails/{{ item.0.name }}/web - state: link - with_subelements: - - "{{ rails_apps }}" - - domains diff --git a/roles/rails/templates/database.yml.j2 b/roles/rails/templates/database.yml.j2 deleted file mode 100644 index 8301c73..0000000 --- a/roles/rails/templates/database.yml.j2 +++ /dev/null @@ -1,7 +0,0 @@ -# {{ ansible_managed }} -production: - adapter: mysql2 - encoding: utf8 - database: {{ ansible_user }}_{{ item.name }} - username: {{ ansible_user }} - password: {{ database_password.stdout }} diff --git a/roles/uberspace/defaults/main.yml b/roles/uberspace/defaults/main.yml new file mode 100644 index 0000000..cbeb29d --- /dev/null +++ b/roles/uberspace/defaults/main.yml @@ -0,0 +1,2 @@ +--- +report_new_mail_passwords: false diff --git a/roles/uberspace/filter_plugins/normalize.py b/roles/uberspace/filter_plugins/normalize.py new file mode 100644 index 0000000..b9c07e2 --- /dev/null +++ b/roles/uberspace/filter_plugins/normalize.py @@ -0,0 +1,59 @@ +from typing import Callable +from collections.abc import Iterable +from typing import Any, Union, Optional +from types import FunctionType +from ansible.plugins.lookup.password import LookupModule as Password +from ansible.parsing.dataloader import DataLoader + + +class FilterModule(object): + ''' jinja2 filters ''' + pw = Password(DataLoader()) + + def filters(self): + return { + 'normalize_item_to_dict': self.normalize_item_to_dict, + 'normalize_items_to_dicts': self.normalize_items_to_dicts, + 'normalize_mailboxes': self.normalize_mailboxes, + } + + def normalize_items_to_dicts(self, items: Iterable, default_key: str, additional_keys: Optional[Iterable[Union[str,Iterable[str,Any]]]]): + def per_item(x): + return self.normalize_item_to_dict(x, default_key, additional_keys) + + return type(items)(map(per_item, items)) + + def normalize_item_to_dict(self, item: Any, default_key: str, additional_keys: Optional[Iterable[Union[str,Iterable[str,Any]]]]): + """ + Apply python string formatting on an object: + .. sourcecode:: jinja + {{ "%s - %s"|format("Hello?", "Foo!") }} + -> Hello? - Foo! + """ + out = dict() + attrs = additional_keys or [] + + #print('normalize_item_to_dict() type(item) is {} (isinstance(item, dict) is {})'.format(type(item), isinstance(item, dict))) + if not isinstance(item, dict): + out[default_key] = item + #print('normalize_item_to_dict() out set to {}'.format(out)) + else: + attrs = [default_key] + attrs + + for additional in attrs: + key = additional if isinstance(additional, str) else additional[0] + val = None if isinstance(additional, str) else additional[1] + + if isinstance(val, FunctionType): + try: + val = val() + except Exception as e: + print('[{}.{}] running function failed! {}'.format(item, key, e)) + pass + + out[key] = item[key] if key in item else val + + return out + + def normalize_mailboxes(self, items: Iterable[Union[str,tuple[str,Any]]], password_spec='/dev/null chars=ascii_letters,digits,.:;-_$%&=# length=16'): + return self.normalize_items_to_dicts(items, 'name', [('password', lambda: self.pw.run([password_spec], [])[0]), ('simple_rules', None)]) diff --git a/roles/uberspace/handlers/main.yml b/roles/uberspace/handlers/main.yml new file mode 100644 index 0000000..62eab3a --- /dev/null +++ b/roles/uberspace/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: Report DNS entries + debug: + msg: "{{ web_domains | default([]) | combine(mail_domains | default([]), recursive=true) }}" + listen: Report DNS entries + +- name: Report new mail passwords + debug: + #msg: "{{ mail_passwords | default([]) }}" + msg: "{{ mail_passwords.results | selectattr('changed') | map(attribute='item') }}" + when: report_new_mail_passwords + listen: Report new mail passwords diff --git a/roles/uberspace/tasks/domains.yml b/roles/uberspace/tasks/domains.yml new file mode 100644 index 0000000..0454a74 --- /dev/null +++ b/roles/uberspace/tasks/domains.yml @@ -0,0 +1,39 @@ +--- +- name: "Enable web for {{ item.name }}" + command: "uberspace web domain add {{ item.name }}" + register: web_domain + failed_when: web_domain.rc == 1 and web_domain.stderr != "Can't add domain to the configuration. It is already configured for your Uberspace account." + changed_when: ("The webserver's configuration has been adapted." in web_domain.stdout_lines) + notify: + - Report DNS entries + when: + - item.web or (item.web is not defined and item.mail is not defined) + - ansible_facts.ansible_local.users[ansible_env.USER].domains[item.name].web is not defined + +- name: "Remember web facts for {{ item.name }}" + set_fact: + web_domains: "{{ (web_domains | default([])) + [{'name': item.name, 'A': web_domain.stdout | regex_search('\\sA\\s+->\\s+([0-9.]+)'), 'AAAA': web_domain.stdout | regex_search('\\sAAAA\\s+->\\s+([a-f0-9:]+)')}] }}" + when: web_domain.changed + +- name: "Enable mail for {{ item.name }}" + command: "uberspace mail domain add {{ item.name }}" + register: _mail_domain + failed_when: _mail_domain.rc == 1 and _mail_domain.stderr != "Can't add domain to the configuration. It is already configured for your Uberspace account." + changed_when: ("The mailserver's configuration has been adapted." in _mail_domain.stdout_lines) + notify: + - Report DNS entries + when: + - item.mail or (item.web is not defined and item.mail is not defined) + - ansible_facts.ansible_local.users[ansible_env.USER].domains[item.name].mail is not defined + +- name: "Remember mail facts for {{ item.name }}" + set_fact: + mail_domains: "{{ (mail_domains | default([])) + [{'name': item.name, 'MX': _mail_domain.stdout | regex_search('\\sMX\\s+->\\s+([^\n]+)'), 'TXT': _mail_domain.stdout | regex_search('\\sTXT\\s+->\\s+([^\n]+)')}] }}" + when: _mail_domain.changed + +- name: "Create symlink for {{ item.name }}" + file: + path: ~/www + src: "/var/www/virtual/{{ ansible_env.USER }}/" + state: link + when: item.link or item.link is not defined diff --git a/roles/uberspace/tasks/mailboxes.yml b/roles/uberspace/tasks/mailboxes.yml new file mode 100644 index 0000000..1534237 --- /dev/null +++ b/roles/uberspace/tasks/mailboxes.yml @@ -0,0 +1,69 @@ +--- +- name: Gather facts + block: + - name: Gather existing mailboxes + command: + cmd: uberspace mail user list + changed_when: false + register: _existing_boxes + + - name: Gather catchall + command: + cmd: uberspace mail catchall status + changed_when: false + register: _catchall + + +- name: Generate new passwords + set_fact: + mail: "{{ mail | combine({'boxes': mail['boxes']|default([]) | normalize_mailboxes}) }}" + +- name: Enable the spamfolder + command: + cmd: uberspace mail spamfolder enable + register: _spamfolder + changed_when: ('already' not in _spamfolder.stdout) + failed_when: _spamfolder.rc != 0 and 'The spam folder is already enabled.' not in _spamfolder.stdout_lines + +- name: Create new mailboxes + command: + cmd: "uberspace mail user add --password {{ item.password }} {{ item.name }}" + register: mail_passwords + changed_when: ("ERROR:" not in mail_passwords.stderr) + failed_when: ("ERROR:" in mail_passwords.stderr and "already exists." not in mail_passwords.stderr) + when: item.name not in _existing_boxes.stdout_lines + with_list: "{{ mail.boxes|default([]) }}" + notify: Report new mail passwords + +- name: Check currently set forward + command: + cmd: "uberspace mail user forward list {{ item.name }}" + register: _current + changed_when: false + failed_when: false + with_list: "{{ mail.forwards|default([]) }}" + +- name: Set forwards + command: + cmd: "uberspace mail user forward set {{ item[0].name }} {{ item[0].target }}" + register: _set_forward + changed_when: item[1] is defined and item[0].target != item[1].stdout_lines[0] + failed_when: ("ERROR:" in _set_forward.stdout) + when: item[1] is not defined or item[0].target != item[1].stdout_lines[0] + with_list: "{{ mail.forwards|default([]) | zip(_current.results) }}" + +- name: Set catchall + command: + cmd: "uberspace mail catchall set {{ mail.catchall }}" + changed_when: mail.catchall != _catchall.stdout + when: mail.catchall is defined and mail.catchall is not false and mail.catchall != _catchall.stdout + +- name: Unset catchall + command: + cmd: uberspace mail catchall del + changed_when: ("No catchall configured." not in _catchall.stdout_lines) + when: mail.catchall is defined and mail.catchall is false + +- name: Set mail rules + include_tasks: sieve.yml + with_list: "{{ mail.boxes|default([]) }}" diff --git a/roles/uberspace/tasks/main.yml b/roles/uberspace/tasks/main.yml new file mode 100644 index 0000000..37aabba --- /dev/null +++ b/roles/uberspace/tasks/main.yml @@ -0,0 +1,38 @@ +--- +- name: Upload local public key + authorized_key: + user: "{{ ansible_env.USER }}" + key: "{{ item }}" + with_items: "{{ upload_ssh_keys }}" + when: upload_ssh_keys is defined and upload_ssh_keys + +- name: Generate SSH key pair for remote user on Uberspace + user: + name: "{{ ansible_env.USER }}" + generate_ssh_key: yes + ssh_key_bits: 4096 + ssh_key_file: .ssh/id_rsa + when: generate_ssh_key + +#- name: Fetch remote user's public key (e.g. for Git access) +# fetch: +# src: ~/.ssh/id_rsa.pub +# dest: "public_keys/{{ ansible_host }}" +# flat: yes + +#- name: Configure Git +# template: +# src: templates/gitconfig.j2 +# dest: ~/.gitconfig + +- name: "Already registered domains:" + debug: + msg: "{{ ansible_facts.ansible_local.users[ansible_env.USER].domains }}" + +- name: Add domains + include_tasks: domains.yml + with_items: "{{ domains }}" + when: domains is defined + +- name: Set up mailboxes + include_tasks: mailboxes.yml diff --git a/roles/uberspace/tasks/sieve.yml b/roles/uberspace/tasks/sieve.yml new file mode 100644 index 0000000..5beaffa --- /dev/null +++ b/roles/uberspace/tasks/sieve.yml @@ -0,0 +1,41 @@ +- block: + - name: Create sieve directory + file: + path: ~/users/{{ item['name'] }}/sieve + state: directory + + - name: Set sieve script + template: + src: simple-sieve-script.sieve.j2 + dest: ~/users/{{ item['name'] }}/sieve/ansible-managed.sieve.test + + - name: Check sieve script validity + command: + cmd: sievec ~/users/{{ item['name'] }}/sieve/ansible-managed.sieve.test + changed_when: false + + - name: Copy tested script over + copy: + src: ~/users/{{ item['name'] }}/sieve/ansible-managed.sieve.test + dest: ~/users/{{ item['name'] }}/sieve/ansible-managed.sieve + remote_src: true + + - name: Activate sieve script + file: + src: ~/users/{{ item['name'] }}/sieve/ansible-managed.sieve + dest: ~/users/{{ item['name'] }}/.dovecot.sieve + state: link + when: ('simple_rules' in item and item.simple_rules is not none) + +- block: + - name: Check active script + stat: + path: ~/users/{{ item['name'] }}/.dovecot.sieve + register: _active_script + + - name: Deactivate sieve script + file: + path: ~/users/{{ item['name'] }}/.dovecot.sieve + state: absent + when: _active_script.stat.exists and _active_script.stat.islnk and _active_script.stat.lnk_target.endswith('/ansible-managed.sieve') + when: ('simple_rules' not in item or item.simple_rules is none) diff --git a/roles/common/templates/gitconfig.j2 b/roles/uberspace/templates/gitconfig.j2 similarity index 100% rename from roles/common/templates/gitconfig.j2 rename to roles/uberspace/templates/gitconfig.j2 diff --git a/roles/uberspace/templates/simple-sieve-script.sieve.j2 b/roles/uberspace/templates/simple-sieve-script.sieve.j2 new file mode 100644 index 0000000..8db8e57 --- /dev/null +++ b/roles/uberspace/templates/simple-sieve-script.sieve.j2 @@ -0,0 +1,102 @@ +# {{ ansible_managed }} + +{% macro get_required(rules) -%} +{%- if list_required(rules).split(',') | difference(['']) | length > 0 -%} +require [{{ [] | union(list_required(rules).split(',')) | difference(['']) | join(",") }}]; +{% endif -%} +{%- endmacro -%} + +{%- macro list_required(rules) -%} +{%- for rule in rules -%} + +{%- if 'if' in rule or 'elif' in rule or 'elsif' in rule or 'elseif' in rule -%} +{%- if (rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif)).startswith('envelope ') -%} +"envelope", +{%- endif -%} +{%- if ' :comparator ' in rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif) -%} + {%- if rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif) | regex_findall(' :comparator "([^"]+)" ')|first not in ["i;octet", "i;ascii-casemap"] -%} +"comparator-{{ rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif) | regex_findall(' :comparator "([^"]+)" ')|first }}", + {%- endif -%} +{%- endif -%} +{%- if ' :value ' in rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif) -%} +"relational", +{%- endif -%} +{%- if (rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif)).startswith('spamtest ') -%} +"spamtestplus", +{%- endif -%} +{%- if (rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif)).startswith('virustest ') -%} +"virustest", +{%- endif -%} +{%- if ' :detail ' in rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif) -%} +"subaddress", +{%- endif -%} +{%- if ' :regex ' in rule.if|default(rule.elif)|default(rule.elsif)|default(rule.elseif) -%} +"regex", +{%- endif -%} +{{ list_required(rule.block) }} + +{%- elif 'else' in rule -%} +{{ list_required(rule.block) }} + +{%- elif 'reject' in rule -%} +"reject", +{%- elif 'fileinto' in rule -%} +"fileinto", +{%- elif 'setflag' in rule -%} +"imap4flags", +{%- elif 'vacation' in rule -%} +"vacation", + +{%- endif -%} +{%- endfor -%} +{%- endmacro -%} + + +{%- macro indent(n) -%} +{{ ' '*n }} +{%- endmacro -%} + +{%- macro makerules(rules, ind) -%} +{%- for subrule in rules -%} +{{ makerule(subrule, ind) }} +{%- endfor -%} +{%- endmacro -%} + +{%- macro makerule(rule, ind) -%} + +{% if 'if' in rule %} + +{{ indent(ind) }}if {{ rule.if }} { +{{ makerules(rule.block, ind+1) }}{{ indent(ind) }}} + +{% elif 'elif' in rule or 'elsif' in rule or 'elseif' in rule %} +{{ indent(ind) }}elsif {{ rule.elif|default(rule.elsif)|default(rule.elseif) }} { +{{ makerules(rule.block, n+1) }}{{ indent(ind) }}} + +{% elif 'else' in rule %} +{{ indent(ind) }}else {{ rule.else }} { +{{ makerules(rule.block, ind+1) }}{{ indent(ind) }}} + +{% elif 'reject' in rule -%} +{{ indent(ind) }}reject "{{ rule.reject }}"; +{% elif 'redirect' in rule -%} +{{ indent(ind) }}redirect "{{ rule.redirect }}"; +{% elif 'keep' in rule -%} +{{ indent(ind) }}keep; +{% elif 'discard' in rule -%} +{{ indent(ind) }}discard; +{% elif 'fileinto' in rule -%} +{{ indent(ind) }}fileinto "{{ rule.fileinto }}"; +{% elif 'setflag' in rule -%} +{{ indent(ind) }}setflag "{{ rule.setflag }}"; +{% elif 'vacation' in rule -%} +{{ indent(ind) }}setflag "{{ rule.vacation }}"; +{% elif 'stop' in rule -%} +{{ indent(ind) }}stop; +{% endif -%} + +{%- endmacro -%} + + +{{ get_required(item.simple_rules) }} +{{ makerules(item.simple_rules, 0) }} diff --git a/roles/uberspace_mail_pw_reset/defaults/main.yml b/roles/uberspace_mail_pw_reset/defaults/main.yml new file mode 100644 index 0000000..a35ef00 --- /dev/null +++ b/roles/uberspace_mail_pw_reset/defaults/main.yml @@ -0,0 +1,10 @@ +--- +project_path: "/home/{{ ansible_env.USER }}/mail_pw_reset" +mail_domain: example.com + +server_name: "{{ ansible_env.USER }}.uber.space" +backend_entrypoint: /mail_reset +backend_port: 1024 + +git_repo: 'https://github.com/PeterNerlich/uberspace_mail_pw_reset.git' +git_version: master diff --git a/roles/uberspace_mail_pw_reset/handlers/main.yml b/roles/uberspace_mail_pw_reset/handlers/main.yml new file mode 100644 index 0000000..6a0bb1b --- /dev/null +++ b/roles/uberspace_mail_pw_reset/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Build translations + ansible.builtin.command: + cmd: "{{ project_path }}/update_and_build_translations.sh" + chdir: "{{ project_path }}" + +- name: Restart flask application + community.general.supervisorctl: + name: uberspace_mail_pw_reset_flask + state: restarted diff --git a/roles/uberspace_mail_pw_reset/tasks/main.yml b/roles/uberspace_mail_pw_reset/tasks/main.yml new file mode 100644 index 0000000..baefaff --- /dev/null +++ b/roles/uberspace_mail_pw_reset/tasks/main.yml @@ -0,0 +1,55 @@ +--- +- name: Fetch flask project + ansible.builtin.git: + repo: "{{ git_repo }}" + dest: "{{ project_path }}" + force: true + version: "{{ git_version }}" + notify: + - Build translations + - Restart flask application + +- name: Create venv + pip: + requirements: "{{ project_path }}/requirements.txt" + virtualenv: "{{ project_path }}" + virtualenv_command: /usr/bin/python3.9 -m venv + environment: + CPATH: /usr/include/python3.9 + notify: Restart flask application + +- name: Create env file + template: + src: env.j2 + dest: "{{ project_path }}/.env" + vars: + secret_key: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits,punctuation length=64') }}" + notify: Restart flask application + +- name: Template uwsgi.ini + template: + src: uwsgi.ini.j2 + dest: "{{ project_path }}/uwsgi.ini" + notify: Restart flask application + +- name: Template flask.ini + template: + src: flask.ini.j2 + dest: ~/etc/services.d/uberspace_mail_pw_reset_flask.ini + notify: Restart flask application + +- name: Start application + community.general.supervisorctl: + name: uberspace_mail_pw_reset_flask + state: started + +- name: Set backend + command: "uberspace web backend set {{ backend_entrypoint }} --http --port {{ backend_port }}" + register: backend + failed_when: backend.rc == 1 and not backend.stdout.startswith("Set backend for ") + changed_when: backend.stdout.startswith("Set backend for ") + when: + - backend_entrypoint not in ansible_facts.ansible_local.users[ansible_env.USER].global_backends + - ansible_facts.ansible_local.users[ansible_env.USER].global_backends[backend_entrypoint].port != backend_port + - ansible_facts.ansible_local.users[ansible_env.USER].global_backends[backend_entrypoint].remove_prefix != false + - ansible_facts.ansible_local.users[ansible_env.USER].global_backends[backend_entrypoint].transport != 'http' diff --git a/roles/uberspace_mail_pw_reset/tasks/setup.yml b/roles/uberspace_mail_pw_reset/tasks/setup.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/uberspace_mail_pw_reset/templates/env.j2 b/roles/uberspace_mail_pw_reset/templates/env.j2 new file mode 100644 index 0000000..e90f030 --- /dev/null +++ b/roles/uberspace_mail_pw_reset/templates/env.j2 @@ -0,0 +1,20 @@ +DATABASE_URI=sqlite:///uberspace_mail_pw_reset.sqlite3 + +SECRET_KEY={{ secret_key }} + +BLACKLISTED_MAILBOXES={{ blacklisted_mailboxes|join(',') }} + +PREFERRED_URL_SCHEME=https +SERVER_NAME={{ server_name }} +APP_ROOT={{ backend_entrypoint }} +SCRIPT_NAME={{ backend_entrypoint }} +MAIL_SENDER=noreply@{{ mail_domain }} +MAIL_RECEIVER_DOMAIN={{ mail_domain }} +MAIL_HOSTNAME={{ ansible_facts.hostname }}.{{ ansible_facts.domain }} + +TOKEN_SECONDS_VALID=3600 # 1 hour +INITIAL_TOKEN_SECONDS_VALID=172800 # 48 hours +TOKEN_MAX_DELAY_TO_RESET=300 # 5 min + +LANGUAGES=en,de +DEFAULT_LOCALE=de diff --git a/roles/uberspace_mail_pw_reset/templates/flask.ini.j2 b/roles/uberspace_mail_pw_reset/templates/flask.ini.j2 new file mode 100644 index 0000000..7c688b4 --- /dev/null +++ b/roles/uberspace_mail_pw_reset/templates/flask.ini.j2 @@ -0,0 +1,3 @@ +[program:uberspace_mail_pw_reset_flask] +directory={{ project_path }} +command={{ project_path }}/bin/uwsgi {{ project_path }}/uwsgi.ini diff --git a/roles/uberspace_mail_pw_reset/templates/uwsgi.ini.j2 b/roles/uberspace_mail_pw_reset/templates/uwsgi.ini.j2 new file mode 100644 index 0000000..5cdad6f --- /dev/null +++ b/roles/uberspace_mail_pw_reset/templates/uwsgi.ini.j2 @@ -0,0 +1,9 @@ +[uwsgi] +mount = {{ backend_entrypoint }}=main:app +manage-script-name = true +pidfile = uberspace_mail_pw_reset.pid +master = true +processes = 1 +http-socket = :{{ backend_port }} +chmod-socket = 660 +vacuum = true diff --git a/roles/common/handlers/main.yml b/roles/uberspace_wordpress_bedrock/handlers/main.yml similarity index 100% rename from roles/common/handlers/main.yml rename to roles/uberspace_wordpress_bedrock/handlers/main.yml diff --git a/roles/uberspace_wordpress_bedrock/tasks/install.yml b/roles/uberspace_wordpress_bedrock/tasks/install.yml new file mode 100644 index 0000000..aa2f254 --- /dev/null +++ b/roles/uberspace_wordpress_bedrock/tasks/install.yml @@ -0,0 +1,185 @@ +--- +- name: Determine docroot + set_fact: + _determined_docroot: "{{ instance.docroot | default(default_root) }}" + vars: + default_root: "/var/www/virtual/{{ ansible_env.USER }}/wordpress/{{ instance.name }}" + +- name: Create PyMySQL venv + pip: + name: + - PyMySQL + virtualenv: ~/.pymysql + virtualenv_command: /usr/bin/python3 -m venv + +- name: Read .my.cnf + block: + - name: Create temporary local file + local_action: + module: tempfile + state: directory + register: mycnf_dir + changed_when: false + + - name: Retrieve .my.cnf + fetch: + src: ~/.my.cnf + dest: "{{ mycnf_dir.path }}" + register: mycnf + changed_when: false + + +- name: Download WP-CLI + block: + - name: Get SHA512 checksum + get_url: + url: https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.sha512 + dest: ~/tmp/wp-cli.phar.sha512 + + - name: Read SHA512 checksum + slurp: + src: ~/tmp/wp-cli.phar.sha512 + register: checksum + + - name: Download WP-CLI + get_url: + url: https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + dest: ~/bin/wp + checksum: "sha512:{{ checksum.content | b64decode }}" + + +- name: Make WP-CLI executable + file: + path: ~/bin/wp + mode: ugo+x + +- name: Install some WP-CLI packages + command: + cmd: "~/bin/wp package install {{ item }}" + creates: "~/.wp-cli/packages/vendor/{{ item }}" + register: install_packages + changed_when: ("Nothing to install, update or remove" not in install_packages.stdout_lines) + with_items: + - aaemnnosttv/wp-cli-dotenv-command:^0.2 + +- name: Create cron job for ~/bin/wp cli update + cron: + name: WP-CLI self-update + job: ~/bin/wp cli update --yes --quiet + special_time: daily + + +#- name: Clone fresh Bedrock (WordPress boilerplate) +# git: +# repo: "{{ instance.bedrock_repo | default('https://github.com/yeah/bedrock.git') }}" +# dest: "{{ _determined_docroot }}" +# version: "{{ instance.bedrock_version | default('master') }}" +# force: yes + +- name: Check if project already exists + stat: + path: "{{ _determined_docroot }}/composer.json" + register: composer_json + +- name: Ensure project dir is created + file: + path: "{{ _determined_docroot }}" + state: directory + when: not composer_json.stat.exists + +- name: Install wordpress bedrock + composer: + command: create-project + arguments: "roots/bedrock {{ _determined_docroot }}" + working_dir: "{{ _determined_docroot }}" + when: not composer_json.stat.exists + +- name: Create MySQL database + mysql_db: + name: "{{ ansible_env.USER }}_{{ instance.name }}" + collation: utf8_unicode_ci + encoding: utf8 + state: present + vars: + ansible_python_interpreter: ~/.pymysql/bin/python3 + +- name: Create .env template with salts + command: + cmd: ~/bin/wp dotenv init --with-salts + chdir: "{{ _determined_docroot }}" + #ignore_errors: yes + register: result + failed_when: ('Environment file already exists' in result.stdout) + changed_when: "'already exists' not in result.stdout" + +- name: Set environment variables in .env + command: + cmd: "~/bin/wp dotenv set {{ item.key }} {{ item.val }}" + chdir: "{{ _determined_docroot }}" + with_items: + - key: "DB_NAME" + val: "{{ ansible_env.USER }}_{{ instance.name }}" + - key: "DB_USER" + val: "{{ lookup('ini', 'user section=client file=' + mycnf.dest) }}" + - key: "DB_PASSWORD" + val: "{{ lookup('ini', 'password section=client file=' + mycnf.dest) }}" + - key: "WP_ENV" + #val: "production" + val: "development" + - key: "WP_HOME" + val: "https://{{ instance.entry_points[0] }}" + - key: "WP_SITEURL" + val: "https://{{ instance.entry_points[0] }}/wp" + + +#- name: Create script ~/bin/wordpress-update +# template: +# src: templates/wordpress-update.j2 +# dest: ~/bin/wordpress-update +# mode: ugo+x +# +#- name: Create runwhen jobs for ~/bin/wordpress-update +# command: +# cmd: "runwhen-conf ~/etc/run-wordpress-update-{{ instance.name }} ~/bin/wordpress-update {{ instance.name }}" +# creates: "~/etc/run-wordpress-update-{{ instance.name }}/run" +# +#- name: Configure runwhen jobs for ~/bin/wordpress-update +# lineinfile: +# dest: ~/etc/run-wordpress-update-{{ instance.name }}/run +# regexp: "^RUNWHEN=" +# line: "RUNWHEN=\",H=1\"" +# +#- name: Activate runwhen jobs for ~/bin/wordpress-update +# file: +# path: "~/service/wordpress-update-{{ instance.name }}" +# src: "~/etc/run-wordpress-update-{{ instance.name }}" +# state: link +# +#- name: Run wordpress-update for the first time +# command: "svc -a ~/service/wordpress-update-{{ instance.name }}" +# +# +#- name: Create webhook for WordPress updates +# template: +# src: templates/wordpress-update.cgi.j2 +# dest: "~/cgi-bin/wordpress-update-{{ instance.name }}.cgi" +# mode: 0755 +# when: "{{ instance.webhook_key is defined }}" + + +- name: Set up .htaccess to redirect all traffic to main domain + blockinfile: + dest: "{{ _determined_docroot }}/web/.htaccess" + create: yes + insertbefore: BOF + block: | + RewriteEngine On + RewriteCond %{HTTP_HOST} !={{ instance.entry_points[0] | regex_replace('/.*') }} + RewriteRule (.*) https://{{ instance.entry_points[0] | regex_replace('/.*') }}/$1 [R=301,L] + +- name: Create domain symlinks + file: + path: "/var/www/virtual/{{ ansible_env.USER }}/{{ item }}" + src: "{{ _determined_docroot }}/web" + state: link + with_items: "{{ instance.entry_points }}" diff --git a/roles/uberspace_wordpress_bedrock/tasks/main.yml b/roles/uberspace_wordpress_bedrock/tasks/main.yml new file mode 100644 index 0000000..b5db11c --- /dev/null +++ b/roles/uberspace_wordpress_bedrock/tasks/main.yml @@ -0,0 +1,7 @@ +--- + +- name: Install wordpress bedrock + include_tasks: install.yml + with_items: "{{ instances }}" + loop_control: + loop_var: instance diff --git a/roles/wordpress/templates/wordpress-update.cgi.j2 b/roles/uberspace_wordpress_bedrock/templates/wordpress-update.cgi.j2 similarity index 74% rename from roles/wordpress/templates/wordpress-update.cgi.j2 rename to roles/uberspace_wordpress_bedrock/templates/wordpress-update.cgi.j2 index 7c8fa27..2aa1517 100644 --- a/roles/wordpress/templates/wordpress-update.cgi.j2 +++ b/roles/uberspace_wordpress_bedrock/templates/wordpress-update.cgi.j2 @@ -5,7 +5,7 @@ echo 'Content-type: text/plain' if [ "${QUERY_STRING}" == "{{ item.webhook_key }}" ]; then echo "Status: 200 OK"; echo - svc -a "/home/{{ ansible_user }}/service/wordpress-update-{{ item.name }}" + svc -a "/home/{{ ansible_env.USER }}/service/wordpress-update-{{ item.name }}" echo "OK." else echo "Status: 403 Forbidden"; echo diff --git a/roles/wordpress/templates/wordpress-update.j2 b/roles/uberspace_wordpress_bedrock/templates/wordpress-update.j2 similarity index 72% rename from roles/wordpress/templates/wordpress-update.j2 rename to roles/uberspace_wordpress_bedrock/templates/wordpress-update.j2 index c3cfd96..3d576ac 100644 --- a/roles/wordpress/templates/wordpress-update.j2 +++ b/roles/uberspace_wordpress_bedrock/templates/wordpress-update.j2 @@ -1,7 +1,7 @@ #!/bin/sh # {{ ansible_managed }} -cd "/var/www/virtual/{{ ansible_user }}/wordpress/$1" +cd "/var/www/virtual/{{ ansible_env.USER }}/wordpress/$1" git fetch origin git reset --hard --quiet origin/master ~/bin/composer -q -n update diff --git a/roles/wordpress/handlers/main.yml b/roles/wordpress/handlers/main.yml deleted file mode 100644 index ed97d53..0000000 --- a/roles/wordpress/handlers/main.yml +++ /dev/null @@ -1 +0,0 @@ ---- diff --git a/roles/wordpress/tasks/main.yml b/roles/wordpress/tasks/main.yml deleted file mode 100644 index dbe2951..0000000 --- a/roles/wordpress/tasks/main.yml +++ /dev/null @@ -1,142 +0,0 @@ ---- -- name: Check if Composer is installed - stat: path=~/bin/composer - register: composer - -- name: Download Composer installer - get_url: url=https://getcomposer.org/installer dest=~/ansible-composer-setup.php - when: composer.stat.exists == False - -- name: Install Composer - shell: php ~/ansible-composer-setup.php --install-dir=$HOME/bin --filename=composer - when: composer.stat.exists == False - -- name: Remove Composer installer - file: path=~/ansible-composer-setup.php state=absent - -- name: Create cron job for ~/bin/composer self-update - cron: - name: composer self-update - job: ~/bin/composer -q -n self-update - special_time: daily - -- name: Download WP-CLI - get_url: url=https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar dest=~/bin/wp force=no - -- name: Make WP-CLI executable - file: - path: ~/bin/wp - mode: ugo+x - -- name: Install some WP-CLI packages - shell: ~/bin/wp package install {{ item }} - args: - creates: ~/.wp-cli/packages/vendor/{{ item }} - with_items: - - aaemnnosttv/wp-cli-dotenv-command:^0.2 - -- name: Create cron job for ~/bin/wp cli update - cron: - name: WP-CLI self-update - job: ~/bin/wp cli update --yes --quiet - special_time: daily - - -- name: Clone fresh Bedrock (WordPress boilerplate) - git: - repo: "{{ item.bedrock_repo }}" - dest: /var/www/virtual/{{ ansible_user }}/wordpress/{{ item.name }} - force: yes - with_items: '{{ wordpress_instances }}' - -- name: Create MySQL database(s) - mysql_db: - name: "{{ ansible_user }}_{{ item.name }}" - collation: utf8_unicode_ci - encoding: utf8 - state: present - with_items: '{{ wordpress_instances }}' - -- name: Create .env template with salts - shell: ~/bin/wp dotenv init --with-salts - args: - chdir: /var/www/virtual/{{ ansible_user }}/wordpress/{{ item.name }} - ignore_errors: yes - register: result - changed_when: "'already exists' not in result.stdout" - with_items: '{{ wordpress_instances }}' - -- name: Read MySQL database password from ~/.my.cnf - shell: sed -n -e '/\[client\]/,$p' ~/.my.cnf | grep password | head -1 | cut -d= -f2 | cut -d " " -f1 warn=no - register: database_password - changed_when: False - -- name: Set environment variables in .env - shell: "~/bin/wp dotenv set DB_NAME {{ ansible_user }}_{{ item.name }}; \ - ~/bin/wp dotenv set DB_USER {{ ansible_user }}; \ - ~/bin/wp dotenv set DB_PASSWORD {{ database_password.stdout }}; \ - ~/bin/wp dotenv set WP_ENV production; \ - ~/bin/wp dotenv set WP_HOME https://{{ item.domains[0] }}; \ - ~/bin/wp dotenv set WP_SITEURL https://{{ item.domains[0] }}/wp;" - args: - chdir: /var/www/virtual/{{ ansible_user }}/wordpress/{{ item.name }} - with_items: '{{ wordpress_instances }}' - -- name: Create script ~/bin/wordpress-update - template: - src: templates/wordpress-update.j2 - dest: ~/bin/wordpress-update - mode: ugo+x - -- name: Create runwhen jobs for ~/bin/wordpress-update - shell: 'runwhen-conf ~/etc/run-wordpress-update-{{ item.name }} ~/bin/wordpress-update {{ item.name }}' - args: - creates: '~/etc/run-wordpress-update-{{ item.name }}/run' - with_items: '{{ wordpress_instances }}' - -- name: Configure runwhen jobs for ~/bin/wordpress-update - lineinfile: - args: - dest: ~/etc/run-wordpress-update-{{ item.name }}/run - regexp: '^RUNWHEN=' - line: 'RUNWHEN=",H=1"' - with_items: '{{ wordpress_instances }}' - -- name: Activate runwhen jobs for ~/bin/wordpress-update - file: - path: '~/service/wordpress-update-{{ item.name }}' - src: '~/etc/run-wordpress-update-{{ item.name }}' - state: link - with_items: '{{ wordpress_instances }}' - -- name: Run wordpress-update for the first time - shell: 'svc -a ~/service/wordpress-update-{{ item.name }}' - with_items: '{{ wordpress_instances }}' - -- name: Create webhook for WordPress updates - template: - src: templates/wordpress-update.cgi.j2 - dest: "~/cgi-bin/wordpress-update-{{ item.name }}.cgi" - mode: 0755 - when: '{{ item.webhook_key is defined }}' - with_items: '{{ wordpress_instances }}' - -- name: Set up .htaccess to redirect all traffic to main domain - blockinfile: - dest: /var/www/virtual/{{ ansible_user }}/wordpress/{{ item.name }}/web/.htaccess - create: yes - insertbefore: BOF - block: | - RewriteEngine On - RewriteCond %{HTTP_HOST} !={{ item.domains[0] | regex_replace('/.*') }} - RewriteRule (.*) https://{{ item.domains[0] | regex_replace('/.*') }}/$1 [R=301,L] - with_items: '{{ wordpress_instances }}' - -- name: Create domain symlinks for wordpresses - file: - path: /var/www/virtual/{{ ansible_user }}/{{ item.1 }} - src: /var/www/virtual/{{ ansible_user }}/wordpress/{{ item.0.name }}/web - state: link - with_subelements: - - "{{ wordpress_instances }}" - - domains diff --git a/site.yml b/site.yml deleted file mode 100644 index df676ee..0000000 --- a/site.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- include: common.yml diff --git a/site.yml.example b/site.yml.example new file mode 100644 index 0000000..4bbca9f --- /dev/null +++ b/site.yml.example @@ -0,0 +1,54 @@ +--- +- hosts: julia.eridanus.uberspace.de + roles: + - role: uberspace + vars: + upload_ssh_keys: + - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + generate_ssh_key: true + domains: + - name: example.com + web: true + mail: true + link: true + - name: www.example.com + web: true + link: true + - name: blog.example.com + web: true + link: true + mail: + boxes: + - name: admin + password: VeryComplicatedPassw0rd + simple_rules: + - if: header :value "ge" :comparator "i;ascii-numeric" "X-Rspamd-Score" "4" + block: + - fileinto: "Spam" + - stop: + - hello + forwards: + - name: hallo + target: hello@example.com + - name: my_dad + target: dad@other.com + catchall: admin + report_new_mail_passwords: true + + - role: uberspace_mail_pw_reset + vars: + mail_domain: example.com + blacklisted_mailboxes: + - admin + + - role: uberspace_wordpress_bedrock + vars: + instances: + - name: example-blog + entry_points: + - blog.example.com + - www.example.com/blog/ + - name: test + docroot: ~/test + bedrock_repo: https://github.com/yeah/bedrock.git + bedrock_version: master diff --git a/uberspaces.example b/uberspaces.example index 165d765..a97523b 100644 --- a/uberspaces.example +++ b/uberspaces.example @@ -1 +1 @@ -julia.eridanus.uberspace.de ansible_user=julia +eridanus.uberspace.de ansible_user=julia ansible_python_interpreter=/usr/bin/python3