diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..aacb20e852 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/.gitattributes export-ignore +/.github export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 5867feed23..0000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -Please read [Contributing to Roots Projects](https://github.com/roots/guidelines/blob/master/CONTRIBUTING.md) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 425c793cf0..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,65 +0,0 @@ -## Submit a feature request or bug report - -- [ ] I've read the [guidelines for Contributing to Roots Projects](https://github.com/roots/guidelines/blob/master/CONTRIBUTING.md) -- [ ] This is a feature request -- [ ] This is a bug report -- [ ] This request isn't a duplicate of an [existing issue](https://github.com/roots/trellis/issues) -- [ ] I've read the [docs](https://roots.io/trellis/docs) and followed them (if applicable) -- [ ] This is not a personal support request that should be posted on the [Roots Discourse](https://discourse.roots.io/c/trellis) forums - -Replace any `X` with your information. - ---- - -**What is the current behavior?** - -X - - -**What is the expected or desired behavior?** - -X - ---- - -## Bug report - -(delete this section if not applicable) - -**Please provide steps to reproduce, including full log output:** - -X - -**Please describe your local environment:** - -Ansible version: X - -OS: X - -Vagrant version: X - -**Where did the bug happen? Development or remote servers?** - -X - -**Please provide a repository or your `wordpress_sites` config (if possible):** - -X - -**Is there a related [Discourse](https://discourse.roots.io/) thread or were any utilized (please link them)?** - -X - ---- - -## Feature Request - -(delete this section if not applicable) - -**Please provide use cases for changing the current behavior:** - -X - -**Other relevant information:** - -X diff --git a/.github/actions/setup-step-ca/action.yml b/.github/actions/setup-step-ca/action.yml new file mode 100644 index 0000000000..1f95236d7f --- /dev/null +++ b/.github/actions/setup-step-ca/action.yml @@ -0,0 +1,42 @@ +name: Setup Step CA ACME server +description: Installs and runs an ACME compatible server via step-ca +inputs: + path: + description: 'step-ca path' + required: false + default: /root/.step +runs: + using: composite + steps: + - name: Set STEP_CA_PATH env + run: echo STEP_CA_PATH=${{ inputs.path }} >> $GITHUB_ENV + shell: bash + - name: Download packages + run: | + wget -q https://dl.step.sm/gh-release/cli/docs-ca-install/v0.18.1/step-cli_0.18.1_amd64.deb + wget -q https://dl.step.sm/gh-release/certificates/docs-ca-install/v0.18.1/step-ca_0.18.1_amd64.deb + shell: bash + - name: Install packages + run: | + sudo dpkg -i step-cli_0.18.1_amd64.deb + sudo dpkg -i step-ca_0.18.1_amd64.deb + shell: bash + - name: Create password file + run: | + sudo mkdir $STEP_CA_PATH && sudo touch $STEP_CA_PATH/password.txt + echo $(openssl rand -hex 12) | sudo tee $STEP_CA_PATH/password.txt + shell: bash + - name: Initialize + run: | + sudo step ca init --name trellis-local-ca --dns 127.0.0.1 --address :8443 --provisioner admin --password-file $STEP_CA_PATH/password.txt --provisioner-password-file $STEP_CA_PATH/password.txt + sudo step ca provisioner add acme --type ACME + shell: bash + - name: Install certificate to system + run: | + sudo step certificate install $STEP_CA_PATH/certs/root_ca.crt + shell: bash + - name: Run service + run: | + sudo cp .github/files/step-ca.service /etc/systemd/system/step-ca.service + sudo systemctl start step-ca + shell: bash diff --git a/.github/files/inventory b/.github/files/inventory new file mode 100644 index 0000000000..c3b78d3ee0 --- /dev/null +++ b/.github/files/inventory @@ -0,0 +1,4 @@ +[production] +localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3 +[web] +localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3 diff --git a/.github/files/step-ca.service b/.github/files/step-ca.service new file mode 100644 index 0000000000..e62cc2bc5d --- /dev/null +++ b/.github/files/step-ca.service @@ -0,0 +1,15 @@ +[Unit] +Description=step-ca service +After=network.target +StartLimitIntervalSec=0 + +[Service] +Type=simple +Restart=always +RestartSec=1 +Environment=STEPPATH=/root/.step +WorkingDirectory=/root/.step +ExecStart=/usr/bin/step-ca config/ca.json --password-file password.txt + +[Install] +WantedBy=multi-user.target diff --git a/.github/files/vault.yml b/.github/files/vault.yml new file mode 100644 index 0000000000..2fe50eb559 --- /dev/null +++ b/.github/files/vault.yml @@ -0,0 +1,48 @@ +# Documentation: https://roots.io/trellis/docs/vault/ +vault_mysql_root_password: productionpw + +# Documentation: https://roots.io/trellis/docs/security/ +vault_users: + - name: "{{ admin_user }}" + password: example_password + salt: "generateme" + +# Variables to accompany `group_vars/production/wordpress_sites.yml` +# Note: the site name (`example.com`) must match up with the site name in the above file. +vault_wordpress_sites: + example.com: + env: + db_password: example_dbpassword + # Generate your keys here: https://roots.io/salts.html + auth_key: "generateme" + secure_auth_key: "generateme" + logged_in_key: "generateme" + nonce_key: "generateme" + auth_salt: "generateme" + secure_auth_salt: "generateme" + logged_in_salt: "generateme" + nonce_salt: "generateme" + example-https.com: + env: + db_password: example_dbpassword + # Generate your keys here: https://roots.io/salts.html + auth_key: "generateme" + secure_auth_key: "generateme" + logged_in_key: "generateme" + nonce_key: "generateme" + auth_salt: "generateme" + secure_auth_salt: "generateme" + logged_in_salt: "generateme" + nonce_salt: "generateme" + redis.example.com: + env: + db_password: example_dbpassword + # Generate your keys here: https://roots.io/salts.html + auth_key: "generateme" + secure_auth_key: "generateme" + logged_in_key: "generateme" + nonce_key: "generateme" + auth_salt: "generateme" + secure_auth_salt: "generateme" + logged_in_salt: "generateme" + nonce_salt: "generateme" diff --git a/.github/files/wordpress_sites.yml b/.github/files/wordpress_sites.yml new file mode 100644 index 0000000000..1d52ba9b6f --- /dev/null +++ b/.github/files/wordpress_sites.yml @@ -0,0 +1,50 @@ +wordpress_sites: + example.com: + site_hosts: + - canonical: example.com + redirects: + - www.example.com + local_path: ../site + repo: git@github.com:roots/bedrock.git + branch: master + multisite: + enabled: false + ssl: + enabled: false + provider: letsencrypt + cache: + enabled: true + example-https.com: + site_hosts: + - canonical: example-https.com + redirects: + - www.example-https.com + local_path: ../site + repo: git@github.com:roots/bedrock.git + branch: master + multisite: + enabled: false + ssl: + enabled: true + provider: letsencrypt + cache: + enabled: false + redis.example.com: + site_hosts: + - canonical: redis.example.com + redirects: + - www.redis.example.com + local_path: ../site + repo: git@github.com:roots/bedrock.git + branch: master + multisite: + enabled: false + ssl: + enabled: false + provider: letsencrypt + cache: + enabled: true + object_cache: + enabled: true + provider: redis + database: 0 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000..d4f07254d5 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base"], + "customManagers": [ + { + "customType": "regex", + "fileMatch": ["^galaxy\\.ya?ml$"], + "matchStrings": [ + "- name: (?[^\\n]+)\\n\\s+src: (?[^\\n]+)\\n\\s+version: (?[^\\n]+)" + ], + "datasourceTemplate": "galaxy", + "versioningTemplate": "loose" + } + ], + "github-actions": { + "enabled": true + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..f13c072c01 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: ci + +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-24.04 + strategy: + matrix: + python-version: ['3.x'] + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + cache: 'pip' + - run: pip install -r requirements.txt + - uses: actions/cache@v5 + with: + path: vendor + key: ${{ runner.os }}-galaxy-${{ hashFiles('galaxy.yml') }} + - run: ansible-galaxy install -r galaxy.yml + - name: Check playbook syntax + run: | + ansible-playbook --syntax-check -e env=development deploy.yml + ansible-playbook --syntax-check -e env=development dev.yml + ansible-playbook --syntax-check -e env=development server.yml + ansible-playbook --syntax-check -e env=development rollback.yml + ansible-playbook --syntax-check -e xdebug_tunnel_inventory_host=1 xdebug-tunnel.yml diff --git a/.github/workflows/discourse.yml b/.github/workflows/discourse.yml new file mode 100644 index 0000000000..0205abbd1f --- /dev/null +++ b/.github/workflows/discourse.yml @@ -0,0 +1,17 @@ +name: Post release topic on Discourse + +on: + release: + types: [published] + +jobs: + post: + runs-on: ubuntu-latest + steps: + - uses: roots/discourse-topic-github-release-action@main + with: + discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }} + discourse-base-url: ${{ secrets.DISCOURSE_BASE_URL }} + discourse-author-username: swalkinshaw + discourse-category: 12 + discourse-tags: releases diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000000..bf32548037 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,111 @@ +name: Integration + +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - run: mkdir $HOME/.ssh + - name: Remove and cleanup mysql + run: | + sudo apt-get remove --purge mysql* + sudo apt-get autoremove + sudo apt-get autoclean + sudo rm -rf /etc/apparmor.d/abstractions/mysql /etc/apparmor.d/cache/usr.sbin.mysqld /etc/mysql /var/lib/mysql /var/log/mysql* /var/log/upstart/mysql.log* /var/run/mysqld ~/.mysql_history + - name: Remove and cleanup Nginx + run: | + sudo apt-get remove --purge nginx* + sudo apt-get autoremove + sudo apt-get autoclean + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: actions/setup-python@v6 + with: + python-version: '3.14' + - uses: ./.github/actions/setup-step-ca + - uses: roots/setup-trellis-cli@v1 + with: + ansible-vault-password: 'fake' + auto-init: false + galaxy-install: false + repo-token: ${{ secrets.GITHUB_TOKEN }} + trellis-directory: '.' + - name: Create new Trellis project + run: trellis new --name example.com --host www.example.com --trellis-version ${{ github.sha }} ./example.com + - name: Update configs + run: | + sudo echo "127.0.0.1 www.example.com example.com www.example-https.com example-https.com www.redis.example.com redis.example.com" | sudo tee -a /etc/hosts + cp ../../.github/files/inventory hosts/production + cp ../../.github/files/wordpress_sites.yml group_vars/production/wordpress_sites.yml + cp ../../.github/files/vault.yml group_vars/production/vault.yml + working-directory: example.com/trellis + - run: trellis exec ansible-playbook --version + working-directory: example.com/trellis + - name: Provision + run: trellis provision --extra-vars "web_user=runner letsencrypt_ca=https://127.0.0.1:8443/acme/acme" production + working-directory: example.com + - name: Deploy non-https site + run: trellis deploy --extra-vars "web_user=runner project_git_repo=https://github.com/roots/bedrock.git" production example.com + working-directory: example.com + - name: Install WordPress + run: | + wp core install --url="http://example.com" --title="Example.com" --admin_user="admin" --admin_password="password" --admin_email="admin@example.com" + working-directory: /srv/www/example.com/current + - name: Verify install + run: curl -s http://example.com | grep "Example" + - name: Deploy previously deployed site + run: trellis deploy --extra-vars "web_user=runner project_git_repo=https://github.com/roots/bedrock.git" production example.com + working-directory: example.com + - name: Deploy https site + run: trellis deploy --extra-vars "web_user=runner project_git_repo=https://github.com/roots/bedrock.git" production example-https.com + working-directory: example.com + - name: Install WordPress + run: | + wp core install --url="http://example-https.com" --title="Example HTTPS" --admin_user="admin" --admin_password="password" --admin_email="admin@example.com" + working-directory: /srv/www/example-https.com/current + - name: Verify install + run: curl -s https://example-https.com | grep "<title>Example HTTPS" + - name: Deploy Redis site + run: trellis deploy --extra-vars "web_user=runner project_git_repo=https://github.com/roots/bedrock.git" production redis.example.com + working-directory: example.com + - name: Install WordPress on Redis site + run: | + wp core install --url="http://redis.example.com" --title="Redis Example" --admin_user="admin" --admin_password="password" --admin_email="admin@example.com" + working-directory: /srv/www/redis.example.com/current + - name: Verify Redis service + run: | + systemctl is-active redis-server + redis-cli ping + - name: Verify PHP Redis extension + run: php -m | grep redis + - name: Test Redis basic functionality + run: | + redis-cli SET test_key "test_value" + redis-cli GET test_key | grep "test_value" + - name: Verify Redis environment variables + run: | + grep "WP_REDIS_HOST" /srv/www/redis.example.com/current/.env + grep "WP_REDIS_PORT" /srv/www/redis.example.com/current/.env + grep "WP_REDIS_DATABASE" /srv/www/redis.example.com/current/.env + - name: Verify Redis site install + run: curl -s http://redis.example.com | grep "<title>Redis Example" + - name: Verify FastCGI cache on example.com + run: | + # First request - check headers + echo "=== First request ===" + curl -I http://example.com | grep -i "fastcgi-cache" + # Second request - check headers + echo "=== Second request ===" + curl -I http://example.com | grep -i "fastcgi-cache" + # Third request - check headers (should be HIT by now) + echo "=== Third request ===" + curl -I http://example.com | grep -iE "fastcgi-cache: (MISS|HIT|STALE)" diff --git a/.github/workflows/wp-cli-version-check.yml b/.github/workflows/wp-cli-version-check.yml new file mode 100644 index 0000000000..ef230fc092 --- /dev/null +++ b/.github/workflows/wp-cli-version-check.yml @@ -0,0 +1,39 @@ +name: WP-CLI version check + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + check-wp-cli-version: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Get latest WP-CLI version + id: get-version + run: | + LATEST_VERSION=$(gh release view --repo wp-cli/wp-cli --json 'tagName' --template '{{slice .tagName 1}}') + echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ github.token }} + + - name: Update wp_cli_version + run: | + sed -i "s/wp_cli_version: .*/wp_cli_version: ${{ steps.get-version.outputs.LATEST_VERSION }}/" roles/wp-cli/defaults/main.yml + + - name: Create pull request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: Update WP-CLI version to ${{ steps.get-version.outputs.LATEST_VERSION }} + title: Update WP-CLI version to ${{ steps.get-version.outputs.LATEST_VERSION }} + body: | + This PR updates the WP-CLI version to the latest release (${{ steps.get-version.outputs.LATEST_VERSION }}). + branch: update-wp-cli-version + delete-branch: true diff --git a/.gitignore b/.gitignore index fd2a86f891..ad8836dc4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .vault_pass -.vagrant vendor/roles *.py[co] *.retry +.trellis/* +!.trellis/cli.yml +trellis.cli.local.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b1da0a6349..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: python -python: 2.7 -sudo: false -cache: pip -install: - - pip install ansible -script: - - ansible-galaxy install --force -r requirements.yml -p vendor/roles - - ansible-playbook --syntax-check -e env=development deploy.yml - - ansible-playbook --syntax-check -e env=development dev.yml - - ansible-playbook --syntax-check -e env=development server.yml diff --git a/.trellis/.gitkeep b/.trellis/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 13240be9de..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,312 +0,0 @@ -### HEAD -* Ansible 2.4 compatibility ([#895](https://github.com/roots/trellis/pull/895)) -* Default h5bp expires and cache busting to false ([#894](https://github.com/roots/trellis/pull/894)) -* Deploys: Update WP theme paths for multisite subsites ([#854](https://github.com/roots/trellis/pull/854)) -* Vagrant: Support DHCP ([#892](https://github.com/roots/trellis/pull/892)) -* Extract Trellis::Config ([#890](https://github.com/roots/trellis/pull/890)) -* Redirect directly to https canonical domain ([#889](https://github.com/roots/trellis/pull/889)) -* WordPress Setup: Add Nginx `ssl_client_certificate` ([#869](https://github.com/roots/trellis/pull/869)) -* Update h5bp/server-configs-nginx ([#876](https://github.com/roots/trellis/pull/876)) -* Update ansible galaxy roles ([#872](https://github.com/roots/trellis/pull/872)) -* Update wp-cli to 1.3.0 ([#871](https://github.com/roots/trellis/pull/871)) -* Add ansible_local support for non-Windows ([#824](https://github.com/roots/trellis/pull/824)) -* Load `modules-enabled` config files in Nginx ([#859](https://github.com/roots/trellis/pull/859)) -* Only include \*.conf files in Nginx `sites-enabled/` ([#862](https://github.com/roots/trellis/pull/862)) -* Add `fastcgi_read_timeout` to Nginx config ([#860](https://github.com/roots/trellis/pull/860)) -* Allow customization of the Nginx package name and PPA ([#858](https://github.com/roots/trellis/pull/858)) -* Nginx microcaching: skip caching WP API requests ([#855](https://github.com/roots/trellis/pull/855)) -* Allow overriding more php-fpm params ([#856](https://github.com/roots/trellis/pull/856)) -* Accommodate child themes: Update WP `stylesheet_root` separately ([#850](https://github.com/roots/trellis/pull/850)) -* Deploys: `--skip-themes` when updating WP `template_root` ([#849](https://github.com/roots/trellis/pull/849)) -* Option to install WP-CLI packages ([#837](https://github.com/roots/trellis/pull/837)) -* Update WP-CLI to 1.2.1 ([#838](https://github.com/roots/trellis/pull/838)) -* Auto-install Vagrant plugins ([#829](https://github.com/roots/trellis/pull/829)) -* Add Vagrant config ([#828](https://github.com/roots/trellis/pull/828)) -* Ansible 2.3 compatibility ([#813](https://github.com/roots/trellis/pull/813)) -* Remove potentially dangerous `db_import` option ([#825](https://github.com/roots/trellis/pull/825)) - -### 1.0.0-rc.1: April 7th, 2017 -* Add vault_wordpress_sites validation ([#823](https://github.com/roots/trellis/pull/823)) -* Use dynamic HostKeyAlgorithms SSH option for unknown hosts ([#798](https://github.com/roots/trellis/pull/798)) -* Accommodate deploy hook vars formatted as lists of includes ([#815](https://github.com/roots/trellis/pull/815)) -* Check Ansible version before Ansible validates task attributes ([#797](https://github.com/roots/trellis/pull/797)) -* Add additional Nginx sites configurations support ([#793](https://github.com/roots/trellis/pull/793)) -* Change `remote-user` role to `connection` role: tests host key, user ([#745](https://github.com/roots/trellis/pull/745)) -* Allow customization of PHP extensions ([#787](https://github.com/roots/trellis/pull/787)) -* Allow for per-project packagist.com authentication ([#762](https://github.com/roots/trellis/pull/762)) -* Set multisite constants false while checking `wp core is-installed` ([#766](https://github.com/roots/trellis/pull/766)) -* Forward extra bin/deploy.sh parameters to ansible-playbook ([#748](https://github.com/roots/trellis/pull/748)) -* Update WP-CLI to 1.1.0 ([#759](https://github.com/roots/trellis/pull/759)) -* Add DOMAIN_CURRENT_SITE to default env variables ([#760](https://github.com/roots/trellis/pull/760)) -* Fix formatting of `set_fact` for `ansible_become_pass` ([#758](https://github.com/roots/trellis/pull/758)) -* Require Ansible 2.2.0.0 or greater ([#726](https://github.com/roots/trellis/pull/726)) -* [BREAKING] Use more secure sshd defaults ([#744](https://github.com/roots/trellis/pull/744)) -* Add basic git repo host keys to `known_hosts` ([#751](https://github.com/roots/trellis/pull/751)) -* Accommodate template inheritance for nginx confs ([#740](https://github.com/roots/trellis/pull/740)) -* Add `apt_packages_custom` to customize Apt packages ([#735](https://github.com/roots/trellis/pull/735)) -* Enable Let's Encrypt to detect updated `site_hosts` ([#630](https://github.com/roots/trellis/pull/630)) -* Add `SKIP_GALAXY` env var to skip galaxy install in Vagrant ([#734](https://github.com/roots/trellis/pull/734)) -* Avoid `loop.first` variable in conditional jinja loops ([#729](https://github.com/roots/trellis/pull/729)) -* Use dynamic `local_path` to accommodate Ansible running on VM ([#725](https://github.com/roots/trellis/pull/725)) -* [BREAKING] Fix #727 - HSTS: default preload to off ([#728](https://github.com/roots/trellis/pull/728)) -* `Vagrantfile`: add automatic support for landrush ([#724](https://github.com/roots/trellis/pull/724)) -* Suppress extra output in SSL certificates ([#723](https://github.com/roots/trellis/pull/723)) -* Fix #718 - improve method of updating theme paths ([#720](https://github.com/roots/trellis/pull/720)) -* Create `/home/vagrant/trellis` bindfs mount with proper permissions ([#705](https://github.com/roots/trellis/pull/705)) - -### 0.9.9: December 14th, 2016 -* Create `project_shared_children` files if they do not exist ([#706](https://github.com/roots/trellis/pull/706)) -* Diffie-Hellman params now conditional on SSL status ([#709](https://github.com/roots/trellis/pull/709)) -* Update PHP to 7.1 ([#695](https://github.com/roots/trellis/pull/695)) -* Update WP-CLI to 1.0.0 ([#708](https://github.com/roots/trellis/pull/708)) -* Ansible-Local for Vagrant boxes on Windows ([#690](https://github.com/roots/trellis/pull/690)) -* Install MariaDB via Ubuntu's official distro packages ([#693](https://github.com/roots/trellis/pull/693)) -* Fix 404s by moving skip_cache conditions to server block ([#692](https://github.com/roots/trellis/pull/692)) -* Nginx includes: Move templates dir, fix 'No such file' error ([#687](https://github.com/roots/trellis/pull/687)) -* [BREAKING] Move shell scripts to bin/ directory ([#680](https://github.com/roots/trellis/pull/680)) -* Add myhostname to nsswitch.conf to ensure resolvable hostname ([#686](https://github.com/roots/trellis/pull/686)) -* Add `bin/xdebug-tunnel.sh` to manage Xdebug and SSH tunnels on remote hosts ([#678](https://github.com/roots/trellis/pull/678)) -* Move Xdebug installation/configuration into its own role ([#678](https://github.com/roots/trellis/pull/678)) -* Disable wp-cron emails ([#685](https://github.com/roots/trellis/pull/685)) -* Make `raw_vars` compatible with play vars and Ansible 2.1 ([#684](https://github.com/roots/trellis/pull/684)) -* Ensure there is always at least one PHP-FPM pool defined ([#682](https://github.com/roots/trellis/pull/682)) -* Update galaxy roles for Ansible 2.2 compatibility ([#681](https://github.com/roots/trellis/pull/681)) -* Update to WP-CLI 0.25.0 for WP 4.7 compat ([#673](https://github.com/roots/trellis/pull/673)) -* Enable per-site setup for permalink structure ([#661](https://github.com/roots/trellis/pull/661)) -* WP 4.6 Compat: set WP_HOME/SITEURL directly ([#647](https://github.com/roots/trellis/pull/647)) -* Create WordPress php-fpm conf after web_root exists ([#642](https://github.com/roots/trellis/pull/642)) -* Fix #637 - Fix condition for permalink structure task ([#643](https://github.com/roots/trellis/pull/643)) -* Fix #639 - WP 4.6 compatibility: update WP-CLI to 0.24.1 ([#640](https://github.com/roots/trellis/pull/640)) - -### 0.9.8: August 14th, 2016 -* Ansible 2.1 compatibility fixes ([#631](https://github.com/roots/trellis/pull/631)) -* [BREAKING] Upgrade Ubuntu from 14.04 Trusty to 16.04 Xenial ([#626](https://github.com/roots/trellis/pull/626)) -* [BREAKING] Add `vault_users` for easier password management ([#614](https://github.com/roots/trellis/pull/614)) -* Fix #581 - Use WP-CLI to run WP cron ([#583](https://github.com/roots/trellis/pull/583)) -* [BREAKING] Require explicit redirects and drop `www_redirect` ([#622](https://github.com/roots/trellis/pull/622)) -* Fix #612 - Bump nginx_fastcgi_buffer_size to `8k` ([#620](https://github.com/roots/trellis/pull/620)) -* Setup permalink structure for multisite installs too ([#617](https://github.com/roots/trellis/pull/617)) -* Fix `wp_home` option in Multisite after install in development ([#616](https://github.com/roots/trellis/pull/616)) -* Add `current_path` var and default to enable custom current release path ([#607](https://github.com/roots/trellis/pull/607)) -* Add Vagrant post up message ([#602](https://github.com/roots/trellis/pull/602)) -* Fix #468 - Use curl to install wp-cli tab completions ([#593](https://github.com/roots/trellis/pull/593)) -* Require Ansible 2.0.2 and remove deploy_helper ([#579](https://github.com/roots/trellis/pull/579)) -* Add connection-related cli options to ping command ([#578](https://github.com/roots/trellis/pull/578)) -* Wrap my.cnf password in quotes ([#577](https://github.com/roots/trellis/pull/577)) -* Update to WP-CLI v0.23.1 ([#576](https://github.com/roots/trellis/pull/576)) -* Fix #563 - Improve remote databases ([#573](https://github.com/roots/trellis/pull/573)) -* Fix #569 - Only skip subdomains for non-www domains ([#570](https://github.com/roots/trellis/pull/570)) -* Enable Let's Encrypt to transition http sites to https ([#565](https://github.com/roots/trellis/pull/565)) - -### 0.9.7: April 10th, 2016 -* Fix #550 - Properly skip permalink setup for MU ([#551](https://github.com/roots/trellis/pull/551)) -* Escape salts and keys to avoid templating errors ([#548](https://github.com/roots/trellis/pull/548)) -* Add plugin to pretty print Ansible msg output ([#544](https://github.com/roots/trellis/pull/544)) -* Fix #482 - Multisite is-installed deploy check ([#543](https://github.com/roots/trellis/pull/543)) -* Skip setting permalink for multisite installs ([#546](https://github.com/roots/trellis/pull/546)) -* Fix #489 - Add $realpath_root to fastcgi_cache_key ([#542](https://github.com/roots/trellis/pull/542)) -* Move modules and plugins to `lib/trellis` directory ([#538](https://github.com/roots/trellis/pull/538)) -* Automatically set `wp_home` and `wp_siteurl` variables ([#533](https://github.com/roots/trellis/pull/533)) -* Switch to Let's Encrypt X3 intermediate certificate and fix chain issues ([#534](https://github.com/roots/trellis/pull/534)) -* Supply better defaults for `db_name` and `db_user` ([#529](https://github.com/roots/trellis/pull/529)) -* Fix deploy env template to use valid ansible vars ([#530](https://github.com/roots/trellis/pull/530)) -* Simplify and improve `wordpress_sites` with better defaults ([#528](https://github.com/roots/trellis/pull/528)) -* Allow option for WinNFSD sync folder provider on Windows ([#527](https://github.com/roots/trellis/pull/527)) -* Improve Let's Encrypt challenge pre-flight tests ([#526](https://github.com/roots/trellis/pull/526)) -* `reverse_www` filter improvements (ignore subdomains) ([#525](https://github.com/roots/trellis/pull/525)) -* Fix deprecation warnings on deploy, use current stable WP-CLI ([#523](https://github.com/roots/trellis/pull/523)) -* Fix #520 - Disable MariaDB binary logging by default ([#521](https://github.com/roots/trellis/pull/521)) -* Let's Encrypt integration ([#518](https://github.com/roots/trellis/pull/518)) -* Improve Git repo format validation ([#516](https://github.com/roots/trellis/pull/516)) -* Fix #505 - Git ignore \*.retry file -* Fix Ansible deprecations for bare variables ([#510](https://github.com/roots/trellis/pull/510)) -* Fixes #508 - update php-xdebug config file path ([#509](https://github.com/roots/trellis/pull/509)) -* Add php-mbstring extension ([#504](https://github.com/roots/trellis/pull/504)) -* Add more necessary PHP extensions ([#503](https://github.com/roots/trellis/pull/503)) - -### 0.9.6: February 18th, 2016 -* Update to latest ansible-role-mailhog version ([#497](https://github.com/roots/trellis/pull/497)) -* Add `reverse_www` filter to fix `www_redirect` ([#486](https://github.com/roots/trellis/pull/486)) -* Add IP address variable, move some variables to top of Vagrantfile ([#494](https://github.com/roots/trellis/pull/494)) -* Keep Composer updated ([#493](https://github.com/roots/trellis/pull/493)) -* Use prestissimo Composer plugin ([#492](https://github.com/roots/trellis/pull/492)) -* Use ansible-role-composer ([#491](https://github.com/roots/trellis/pull/491)) -* Fix bad `curl` output ([#490](https://github.com/roots/trellis/pull/490)) -* Fixes #410 - Default to 1 CPU in Vagrant ([#487](https://github.com/roots/trellis/pull/487)) - -### 0.9.5: February 10th, 2016 -* Fix Nginx includes for Ansible 2.0 ([#473](https://github.com/roots/trellis/pull/473)) -* Use `ondrej/php` PPA since `ondrej/php-7.0` is deprecated ([#479](https://github.com/roots/trellis/pull/479)) -* Fix Ansible 2.x deploys and require version 2.x ([#478](https://github.com/roots/trellis/pull/478)) -* Update to PHP 7.0 and remove HHVM ([#432](https://github.com/roots/trellis/pull/432)) -* Windows: Sync `hosts` dir with proper permissions ([#460](https://github.com/roots/trellis/pull/460)) -* Fix `inventory_file` variable in connection tests ([#470](https://github.com/roots/trellis/pull/470)) -* Fix conditional logic for permalink setup task ([#467](https://github.com/roots/trellis/pull/467)) -* Fix permalink setup during WordPress Install ([#466](https://github.com/roots/trellis/pull/466)) -* Fix deploy pre-flight check for verifying repo ([#463](https://github.com/roots/trellis/pull/463)) -* Ansible 2.0 compatibility ([#461](https://github.com/roots/trellis/pull/461)) -* Add pre-flight checks for common deploy problems ([#459](https://github.com/roots/trellis/pull/459)) -* Prevent duplicate hosts entries made by `vagrant-hostsupdater` ([#458](https://github.com/roots/trellis/pull/458)) -* Fix README's `ansible-playbook` command for server.yml ([#456](https://github.com/roots/trellis/pull/456)) -* Fix development hosts file ([#455](https://github.com/roots/trellis/pull/455)) -* Add tags to select includes and tasks ([#453](https://github.com/roots/trellis/pull/453)) -* Improve Git deploy implementation via `git archive` ([#451](https://github.com/roots/trellis/pull/451)) -* Replace strip_www with optional redirect to www/non-www ([#452](https://github.com/roots/trellis/pull/452)) -* Accommodate file encryption via ansible vault ([#317](https://github.com/roots/trellis/pull/317)) -* Fixes #353 - Allow insecure curl reqs for cron ([#450](https://github.com/roots/trellis/pull/450)) -* Fixes #374 - Remove composer vendor/bin from $PATH ([#449](https://github.com/roots/trellis/pull/449)) -* Refactor hosts files ([#313](https://github.com/roots/trellis/pull/313)) -* Fixes #436 - Let WP handle 404s for PHP files ([#448](https://github.com/roots/trellis/pull/448)) -* Fixes #297 - Use `php_flag` vs `php_admin_flag` ([#447](https://github.com/roots/trellis/pull/447)) -* Fixes #316 - Set WP permalink structure during install ([#316](https://github.com/roots/trellis/pull/316)) -* Switch to https://api.ipify.org for IP lookup ([#444](https://github.com/roots/trellis/pull/444)) -* Replace `vagrant-hostsupdater` with `vagrant-hostmanager` ([#442](https://github.com/roots/trellis/pull/442)) -* Switch to mainline Nginx, replaces SPDY with HTTP2 ([#389](https://github.com/roots/trellis/issues/389)) -* Add `wp core update-db` to deploy finalize hook ([#411](https://github.com/roots/trellis/pull/411)) -* Use WP-CLI 0.21.1 ([#392](https://github.com/roots/trellis/pull/392)) -* Add variable for whitelisted IPs ([#435](https://github.com/roots/trellis/pull/435)) - -### 0.9.3: November 29th, 2015 -* Nginx role improvements: use more h5bp configs ([#428](https://github.com/roots/trellis/pull/428)) -* Add global `deploy_before` and `deploy_after` hooks ([#427](https://github.com/roots/trellis/pull/427)) -* Fix HSTS headers ([#424](https://github.com/roots/trellis/pull/424)) -* Notify Windows users about SSH forwarding ([#423](https://github.com/roots/trellis/pull/423)) -* Use append_privs for WP DB privileges ([#422](https://github.com/roots/trellis/pull/422)) -* Stop WP cron job emails ([#421](https://github.com/roots/trellis/pull/421)) -* Add WP-CLI bash completion script ([#407](https://github.com/roots/trellis/pull/407)) -* Add Composer config `github-oauth` variable ([#402](https://github.com/roots/trellis/pull/402)) -* Fix copy project local files in example hook ([#404](https://github.com/roots/trellis/pull/404)) -* Update cron variable to match Bedrock ([#394](https://github.com/roots/trellis/pull/394)) -* Add deploy_build_before example hook for theme assets ([#397](https://github.com/roots/trellis/pull/37)) -* Use curl instead of dig for IP lookups ([#390](https://github.com/roots/trellis/pull/390)) -* Update SSL cipher suite ([#386](https://github.com/roots/trellis/pull/386)) -* Support for other Vagrant providers (VirtualBox, VMWare, Parallels) ([#340](https://github.com/roots/trellis/pull/340)) -* Specify versions for Ansible Galaxy requirements ([#385](https://github.com/roots/trellis/pull/385)) -* Adds ability to configure [HSTS headers](https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security) with site variables. ([#388](https://github.com/roots/trellis/pull/388)) - -### 0.9.2: October 15th, 2015 -* Add dev's IP to ferm whitelist ([#381](https://github.com/roots/trellis/pull/381)) -* Add nonempty option to config.bindfs.bind_folder ([#382](https://github.com/roots/trellis/pull/382)) -* Add proper hooks for task files during deploys ([#378](https://github.com/roots/trellis/pull/378)) -* Fix logrotate's Nginx postrotate script ([#377](https://github.com/roots/trellis/pull/377)) -* Add static HTML files as fallbacks for Nginx's `index` directive ([#376](https://github.com/roots/trellis/pull/376)) -* Use Windows environment variable to determine number of CPUs ([#366](https://github.com/roots/trellis/pull/366)) -* Check for galaxy roles before `vagrant up` ([#365](https://github.com/roots/trellis/pull/365)) -* Install Xdebug by default in development environment ([#363](https://github.com/roots/trellis/pull/363)) -* Ensure admin_user can connect before disabling root ([#345](https://github.com/roots/trellis/pull/345)) -* Prevent PHP execution in uploads directory ([#356](https://github.com/roots/trellis/pull/356)) -* Update h5bp Nginx configs ([#355](https://github.com/roots/trellis/pull/355)) -* Convert sshd role variables to booleans ([#344](https://github.com/roots/trellis/pull/344)) -* Add check to validate `subtree_path` during deploy ([#334](https://github.com/roots/trellis/pull/334)) -* Rename WP site variable `subtree` to `subtree_path` ([#329](https://github.com/roots/trellis/pull/329)) -* Add extra HTTP security headers ([#322](https://github.com/roots/trellis/pull/322)) -* HHVM restart cron job fix ([#327](https://github.com/roots/trellis/pull/327)) -* Improve SSH remote user detection ([#321](https://github.com/roots/trellis/pull/321)) -* Add variable + better default for Nginx fastcgi buffers ([#302](https://github.com/roots/trellis/pull/302)) -* WP Multisite install fixes ([#319](https://github.com/roots/trellis/pull/319)) -* Re-organize `group_vars` files into subdirectories and separate files ([#308](https://github.com/roots/trellis/pull/308)) - -### 0.9.1: August 18th, 2015 -* Capture development mail with MailHog ([#304](https://github.com/roots/trellis/pull/304)) -* Update git remote URL before cloning on deploys ([#299](https://github.com/roots/trellis/pull/299)) -* Allow user to set the timezone ([#301](https://github.com/roots/trellis/pull/301)) -* Improvements to custom Nginx includes ([#242](https://github.com/roots/trellis/pull/242)) -* Fix comment in Vagrantfile: use absolute path for ANSIBLE_PATH ([#292](https://github.com/roots/trellis/pull/292)) -* Fix remote user handling for AWS ([#290](https://github.com/roots/trellis/pull/290)) - -### 0.9.0: August 3rd, 2015 -* Allow auto-generation of self signed SSL certificate -* Merge secure-root.yml into server.yml -* Bump Ansible requirement to >= 1.9.2 -* Validate that at least the minimum required version of Ansible is used -* Fix PHP error handling -* Flush wp db theme roots on deploy -* Stop recursive copying of vendor -* Update the bin/windows.sh script with absolute path -* Conditionally copy .env into web root -* Add subtree commented out -* Add Composer binary path to the default path -* Change base box to stock Ubuntu 14.04 -* Rename bedrock-ansible to Trellis -* Restore strip_www functionality -* Protect against Logjam attack by generating a strong and unique Diffie-Hellman group -* Move SSH key handling to users role -* Fix multisite conditional in wordpress-site.conf -* Allow use of FastCGI caching -* Wrap octal mode in quotes -* Fix project_shared_children mode defaults -* Allow for custom permissions for shared sources -* Provide a mechanism for custom Nginx includes -* Add trailing slash to WP core rewrite, preventing possible redirect loop -* Insert full path to service command, add hhvm restart minute -* Disable exposing PHP version to the world -* wordpress-install improvements -* Nginx h5bp config improvements -* Make composer self-update idempotent -* Fix project_subtree conditional -* Remove redundant site_name when naming log files -* Fix project_subtree check -* Fix conditional check for multi-site deploys -* Fix .env generation for wordpress-install -* Mirror `server_name` in SSL and non-SSL blocks -* Windows compatibility -* Add swapfile role -* Nginx: better worker_processes setting -* Use inventory_hostname instead of ansible_hostname -* Update Ansible version requirements -* Add information on how to deploy with the git strategy -* Define provider as virtualbox to avoid failure -* Don't set HSTS header over HTTP -* Add note about generating keys from the WordPress API -* Use site instead of example.com -* Be consistent with roots-example-project repo -* Add vagrant-hostsupdater to requirements -* SSL support -* Vagrant: resolve site paths relative to Ansible -* Subtree should be defined on a site -* Remove static IP from site_hosts -* Deploy improvements -* WP subdomain multisite support -* Add xdebug role -* Add logrotate role -* Add ntpd role -* Ansible deploys -* HHVM implementation -* Add SMTP role -* Install php5-memcached -* Update to PHP 5.6 -* Simplify Vagrantfile -* Add better SSH defaults -* Add fail2ban, ferm for added security -* Remove naming restriction on Bedrock path -* Add vagrant-bindfs for custom NFS permissions -* Limit `sendfile off` directive to development env -* Add better upload size and execution time defaults -* Use H5BP server configs -* Hardcode Vagrant VM memory to 1GB -* Replace dots in cron file names -* Use NFS for shared folders and better performance -* Tagged playbook roles - -### 0.4.0: September 9th, 2014 -* Complete memcached implementation -* Better PHP production configs: errors and opcache -* Always set fastcgi param `SCRIPT_FILENAME` in Nginx for better version compatibility - -### 0.3.0: August 20th, 2014 -* Ansible 1.6.8 compatibility (bug fix) -* Fix for slow network connections -* Nginx reload after DB import -* Integrate vagrant-hostsupdater -* Improve organization and file/folder structure -* MySQL password support -* Memcached role -* Improved hosts file and group_vars for separate environments - -### 0.2.0: May 15th, 2014 -* Add roots/bedrock Vagrant box -* Add `run_composer` option to `wordpress_sites` so Composer can be run on the VM removing the requirement for it on the host -* Remove upgrade role since we can't control package versions with it - -### 0.1.1: May 1st, 2014 -* Initial release diff --git a/LICENSE.md b/LICENSE.md index ee859d0f58..9e47289f33 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) Roots +Copyright (c) Roots Software LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 796f26f4d7..bf51c4ccb4 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,46 @@ -# Trellis -[![Release](https://img.shields.io/github/release/roots/trellis.svg?style=flat-square)](https://github.com/roots/trellis/releases) -[![Build Status](https://img.shields.io/travis/roots/trellis.svg?style=flat-square)](https://travis-ci.org/roots/trellis) +<p align="center"> + <a href="https://roots.io/trellis/"> + <img alt="Trellis" src="https://cdn.roots.io/app/uploads/logo-trellis.svg" height="100"> + </a> +</p> -Ansible playbooks for setting up a LEMP stack for WordPress. +<p align="center"> + <a href="https://github.com/roots/trellis/actions/workflows/ci.yml"><img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/roots/trellis/ci.yml?branch=master&logo=github&label=CI&style=flat-square"></a> + <a href="https://twitter.com/rootswp"><img alt="Follow Roots" src="https://img.shields.io/badge/follow%20@rootswp-1da1f2?logo=twitter&logoColor=ffffff&message=&style=flat-square"></a> + <a href="https://github.com/sponsors/roots"><img src="https://img.shields.io/badge/sponsor%20roots-525ddc?logo=github&style=flat-square&logoColor=ffffff&message=" alt="Sponsor Roots"></a> +</p> -- Local development environment with Vagrant -- High-performance production servers -- One-command deploys for your [Bedrock](https://roots.io/bedrock/)-based WordPress sites - -## What's included - -Trellis will configure a server with the following and more: - -* Ubuntu 16.04 Xenial LTS -* Nginx (with optional FastCGI micro-caching) -* PHP 7.1 -* MariaDB (a drop-in MySQL replacement) -* SSL support (scores an A+ on the [Qualys SSL Labs Test](https://www.ssllabs.com/ssltest/)) -* Let's Encrypt integration for free SSL certificates -* HTTP/2 support (requires SSL) -* Composer -* WP-CLI -* sSMTP (mail delivery) -* MailHog -* Memcached -* Fail2ban -* ferm - -## Documentation - -Full documentation is available at [https://roots.io/trellis/docs/](https://roots.io/trellis/docs/). - -## Requirements - -Make sure all dependencies have been installed before moving on: - -* [Virtualbox](https://www.virtualbox.org/wiki/Downloads) >= 4.3.10 -* [Vagrant](https://www.vagrantup.com/downloads.html) >= 1.8.5 - -## Installation +<p align="center">Ansible-powered LEMP stack for WordPress</strong></p> -The recommended directory structure for a Trellis project looks like: +<p align="center"> + <a href="https://roots.io/trellis/">Website</a>    <a href="https://roots.io/trellis/docs/installation/">Documentation</a>    <a href="https://github.com/roots/trellis/releases">Releases</a>    <a href="https://discourse.roots.io/">Community</a> +</p> -```shell -example.com/ # → Root folder for the project -├── trellis/ # → Your clone of this repository -└── site/ # → A Bedrock-based WordPress site - └── web/ - ├── app/ # → WordPress content directory (themes, plugins, etc.) - └── wp/ # → WordPress core (don't touch!) -``` +## Support us -See a complete working example in the [roots-example-project.com repo](https://github.com/roots/roots-example-project.com). +We're dedicated to pushing modern WordPress development forward through our open source projects, and we need your support to keep building. You can support our work by purchasing [Radicle](https://roots.io/radicle/), our recommended WordPress stack, or by [sponsoring us on GitHub](https://github.com/sponsors/roots). Every contribution directly helps us create better tools for the WordPress ecosystem. -1. Create a new project directory: `$ mkdir example.com && cd example.com` -2. Clone Trellis: `$ git clone --depth=1 git@github.com:roots/trellis.git && rm -rf trellis/.git` -3. Clone Bedrock: `$ git clone --depth=1 git@github.com:roots/bedrock.git site && rm -rf site/.git` +### Sponsors -Windows user? [Read the Windows docs](https://roots.io/trellis/docs/windows/) for slightly different installation instructions. VirtualBox is known to have poor performance in Windows — use VMware or [see some possible solutions](https://discourse.roots.io/t/virtualbox-performance-in-windows/3932). +<a href="https://carrot.com/"><img src="https://cdn.roots.io/app/uploads/carrot.svg" alt="Carrot" width="120" height="90"></a> <a href="https://wordpress.com/"><img src="https://cdn.roots.io/app/uploads/wordpress.svg" alt="WordPress.com" width="120" height="90"></a> <a href="https://www.itineris.co.uk/"><img src="https://cdn.roots.io/app/uploads/itineris.svg" alt="Itineris" width="120" height="90"></a> <a href="https://kinsta.com/?kaid=OFDHAJIXUDIV"><img src="https://cdn.roots.io/app/uploads/kinsta.svg" alt="Kinsta" width="120" height="90"></a> -## Local development setup +## Overview -1. Configure your WordPress sites in `group_vars/development/wordpress_sites.yml` and in `group_vars/development/vault.yml` -2. Run `vagrant up` +Trellis is a collection of Ansible playbooks for setting up a LEMP stack for WordPress. -[Read the local development docs](https://roots.io/trellis/docs/local-development-setup/) for more information. - -## Remote server setup (staging/production) - -For remote servers, installing Ansible locally is an additional requirement. See the [docs](https://roots.io/trellis/docs/remote-server-setup/#requirements) for more information. - -A base Ubuntu 16.04 server is required for setting up remote servers. OS X users must have [passlib](http://pythonhosted.org/passlib/install.html#installation-instructions) installed. - -1. Configure your WordPress sites in `group_vars/<environment>/wordpress_sites.yml` and in `group_vars/<environment>/vault.yml` (see the [Vault docs](https://roots.io/trellis/docs/vault/) for how to encrypt files containing passwords) -2. Add your server IP/hostnames to `hosts/<environment>` -3. Specify public SSH keys for `users` in `group_vars/all/users.yml` (see the [SSH Keys docs](https://roots.io/trellis/docs/ssh-keys/)) -4. Run `ansible-playbook server.yml -e env=<environment>` to provision the server - -[Read the remote server docs](https://roots.io/trellis/docs/remote-server-setup/) for more information. - -## Deploying to remote servers - -1. Add the `repo` (Git URL) of your Bedrock WordPress project in the corresponding `group_vars/<environment>/wordpress_sites.yml` file -2. Set the `branch` you want to deploy -3. Run `./bin/deploy.sh <environment> <site name>` -4. To rollback a deploy, run `ansible-playbook rollback.yml -e "site=<site name> env=<environment>"` - -[Read the deploys docs](https://roots.io/trellis/docs/deploys/) for more information. - -## Contributing +- Local development environment with Vagrant +- High-performance production servers +- Zero-downtime deploys for your [Bedrock](https://roots.io/bedrock/)-based WordPress sites +- [trellis-cli](https://github.com/roots/trellis-cli) for easier management -Contributions are welcome from everyone. We have [contributing guidelines](https://github.com/roots/guidelines/blob/master/CONTRIBUTING.md) to help you get started. +## Getting Started -## Community +See the [Trellis installation documentation](https://roots.io/trellis/docs/installation/). -Keep track of development and community news. +## Stay Connected -* Participate on the [Roots Discourse](https://discourse.roots.io/) -* Follow [@rootswp on Twitter](https://twitter.com/rootswp) -* Read and subscribe to the [Roots Blog](https://roots.io/blog/) -* Subscribe to the [Roots Newsletter](https://roots.io/subscribe/) -* Listen to the [Roots Radio podcast](https://roots.io/podcast/) +- Join us on Discord by [sponsoring us on GitHub](https://github.com/sponsors/roots) +- Participate on [Roots Discourse](https://discourse.roots.io/) +- Follow [@rootswp on Twitter](https://twitter.com/rootswp) +- Read the [Roots Blog](https://roots.io/blog/) +- Subscribe to the [Roots Newsletter](https://roots.io/newsletter/) diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index f50dce7543..0000000000 --- a/Vagrantfile +++ /dev/null @@ -1,165 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -ANSIBLE_PATH = __dir__ # absolute path to Ansible directory on host machine -ANSIBLE_PATH_ON_VM = '/home/vagrant/trellis' # absolute path to Ansible directory on virtual machine - -require File.join(ANSIBLE_PATH, 'lib', 'trellis', 'vagrant') -require File.join(ANSIBLE_PATH, 'lib', 'trellis', 'config') -require 'yaml' - -vconfig = YAML.load_file("#{ANSIBLE_PATH}/vagrant.default.yml") - -if File.exist?("#{ANSIBLE_PATH}/vagrant.local.yml") - local_config = YAML.load_file("#{ANSIBLE_PATH}/vagrant.local.yml") - vconfig.merge!(local_config) if local_config -end - -ensure_plugins(vconfig.fetch('vagrant_plugins')) if vconfig.fetch('vagrant_install_plugins') - -trellis_config = Trellis::Config.new(root_path: ANSIBLE_PATH) - -Vagrant.require_version '>= 1.8.5' - -Vagrant.configure('2') do |config| - config.vm.box = vconfig.fetch('vagrant_box') - config.vm.box_version = vconfig.fetch('vagrant_box_version') - config.ssh.forward_agent = true - config.vm.post_up_message = post_up_message - - # Fix for: "stdin: is not a tty" - # https://github.com/mitchellh/vagrant/issues/1673#issuecomment-28288042 - config.ssh.shell = %{bash -c 'BASH_ENV=/etc/profile exec bash'} - - # Required for NFS to work - if vconfig.fetch('vagrant_ip') == 'dhcp' - config.vm.network :private_network, type: 'dhcp', hostsupdater: 'skip' - - cached_addresses = {} - config.hostmanager.ip_resolver = proc do |vm, _resolving_vm| - if cached_addresses[vm.name].nil? - if vm.communicate.ready? - vm.communicate.execute("hostname -I | cut -d ' ' -f 2") do |type, contents| - cached_addresses[vm.name] = contents.split("\n").first[/(\d+\.\d+\.\d+\.\d+)/, 1] - end - end - end - cached_addresses[vm.name] - end - else - config.vm.network :private_network, ip: vconfig.fetch('vagrant_ip'), hostsupdater: 'skip' - end - - main_hostname, *hostnames = trellis_config.site_hosts_canonical - config.vm.hostname = main_hostname - - if Vagrant.has_plugin?('vagrant-hostmanager') && !trellis_config.multisite_subdomains? - redirects = trellis_config.site_hosts_redirects - - config.hostmanager.enabled = true - config.hostmanager.manage_host = true - config.hostmanager.aliases = hostnames + redirects - elsif Vagrant.has_plugin?('landrush') && trellis_config.multisite_subdomains? - config.landrush.enabled = true - config.landrush.tld = config.vm.hostname - hostnames.each { |host| config.landrush.host host, vconfig.fetch('vagrant_ip') } - else - fail_with_message "vagrant-hostmanager missing, please install the plugin with this command:\nvagrant plugin install vagrant-hostmanager\n\nOr install landrush for multisite subdomains:\nvagrant plugin install landrush" - end - - bin_path = File.join(ANSIBLE_PATH_ON_VM, 'bin') - - if Vagrant::Util::Platform.windows? and !Vagrant.has_plugin? 'vagrant-winnfsd' - trellis_config.wordpress_sites.each_pair do |name, site| - config.vm.synced_folder local_site_path(site), remote_site_path(name, site), owner: 'vagrant', group: 'www-data', mount_options: ['dmode=776', 'fmode=775'] - end - - config.vm.synced_folder ANSIBLE_PATH, ANSIBLE_PATH_ON_VM, mount_options: ['dmode=755', 'fmode=644'] - config.vm.synced_folder File.join(ANSIBLE_PATH, 'bin'), bin_path, mount_options: ['dmode=755', 'fmode=755'] - else - if !Vagrant.has_plugin? 'vagrant-bindfs' - fail_with_message "vagrant-bindfs missing, please install the plugin with this command:\nvagrant plugin install vagrant-bindfs" - else - trellis_config.wordpress_sites.each_pair do |name, site| - config.vm.synced_folder local_site_path(site), nfs_path(name), type: 'nfs' - config.bindfs.bind_folder nfs_path(name), remote_site_path(name, site), u: 'vagrant', g: 'www-data', o: 'nonempty' - end - - config.vm.synced_folder ANSIBLE_PATH, '/ansible-nfs', type: 'nfs' - config.bindfs.bind_folder '/ansible-nfs', ANSIBLE_PATH_ON_VM, o: 'nonempty', p: '0644,a+D' - config.bindfs.bind_folder bin_path, bin_path, perms: '0755' - end - end - - vconfig.fetch('vagrant_synced_folders', []).each do |folder| - options = { - type: folder.fetch('type', 'nfs'), - create: folder.fetch('create', false), - mount_options: folder.fetch('mount_options', []) - } - - destination_folder = folder.fetch('bindfs', true) ? nfs_path(folder['destination']) : folder['destination'] - - config.vm.synced_folder folder['local_path'], destination_folder, options - - if folder.fetch('bindfs', true) - config.bindfs.bind_folder destination_folder, folder['destination'], folder.fetch('bindfs_options', {}) - end - end - - provisioner = local_provisioning? ? :ansible_local : :ansible - provisioning_path = local_provisioning? ? ANSIBLE_PATH_ON_VM : ANSIBLE_PATH - - config.vm.provision provisioner do |ansible| - if local_provisioning? - ansible.install_mode = 'pip' - ansible.provisioning_path = provisioning_path - ansible.version = vconfig.fetch('vagrant_ansible_version') - end - - ansible.playbook = File.join(provisioning_path, 'dev.yml') - ansible.galaxy_role_file = File.join(provisioning_path, 'requirements.yml') unless vconfig.fetch('vagrant_skip_galaxy') || ENV['SKIP_GALAXY'] - ansible.galaxy_roles_path = File.join(provisioning_path, 'vendor/roles') - - ansible.groups = { - 'web' => ['default'], - 'development' => ['default'] - } - - ansible.tags = ENV['ANSIBLE_TAGS'] - ansible.extra_vars = { 'vagrant_version' => Vagrant::VERSION } - - if vars = ENV['ANSIBLE_VARS'] - extra_vars = Hash[vars.split(',').map { |pair| pair.split('=') }] - ansible.extra_vars.merge!(extra_vars) - end - end - - # Virtualbox settings - config.vm.provider 'virtualbox' do |vb| - vb.name = config.vm.hostname - vb.customize ['modifyvm', :id, '--cpus', vconfig.fetch('vagrant_cpus')] - vb.customize ['modifyvm', :id, '--memory', vconfig.fetch('vagrant_memory')] - - # Fix for slow external network connections - vb.customize ['modifyvm', :id, '--natdnshostresolver1', 'on'] - vb.customize ['modifyvm', :id, '--natdnsproxy1', 'on'] - end - - # VMware Workstation/Fusion settings - ['vmware_fusion', 'vmware_workstation'].each do |provider| - config.vm.provider provider do |vmw, override| - vmw.name = config.vm.hostname - vmw.vmx['numvcpus'] = vconfig.fetch('vagrant_cpus') - vmw.vmx['memsize'] = vconfig.fetch('vagrant_memory') - end - end - - # Parallels settings - config.vm.provider 'parallels' do |prl, override| - prl.name = config.vm.hostname - prl.cpus = vconfig.fetch('vagrant_cpus') - prl.memory = vconfig.fetch('vagrant_memory') - prl.update_guest_tools = true - end -end diff --git a/ansible.cfg b/ansible.cfg index 3f42017488..87df7f1376 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,15 +1,14 @@ [defaults] -callback_plugins = ~/.ansible/plugins/callback_plugins/:/usr/share/ansible_plugins/callback_plugins:lib/trellis/plugins/callback -stdout_callback = output -filter_plugins = ~/.ansible/plugins/filter_plugins/:/usr/share/ansible_plugins/filter_plugins:lib/trellis/plugins/filter +callback_plugins = ~/.ansible/plugins/callback:/usr/share/ansible/plugins/callback:lib/trellis/plugins/callback +filter_plugins = ~/.ansible/plugins/filter:/usr/share/ansible/plugins/filter:lib/trellis/plugins/filter force_color = True force_handlers = True inventory = hosts nocows = 1 roles_path = vendor/roles -vars_plugins = ~/.ansible/plugins/vars_plugins/:/usr/share/ansible_plugins/vars_plugins:lib/trellis/plugins/vars +vars_plugins = ~/.ansible/plugins/vars:/usr/share/ansible/plugins/vars:lib/trellis/plugins/vars +pipelining = True [ssh_connection] -ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s -pipelining = True +ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s -o HostKeyAlgorithms=ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,ssh-rsa retries = 1 diff --git a/bin/deploy.sh b/bin/deploy.sh deleted file mode 100755 index 79d72c37d4..0000000000 --- a/bin/deploy.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -shopt -s nullglob - -ENVIRONMENTS=( hosts/* ) -ENVIRONMENTS=( "${ENVIRONMENTS[@]##*/}" ) - -show_usage() { - echo "Usage: deploy <environment> <site name> [options] - -<environment> is the environment to deploy to ("staging", "production", etc) -<site name> is the WordPress site to deploy (name defined in "wordpress_sites") -[options] is any number of parameters that will be passed to ansible-playbook - -Available environments: -`( IFS=$'\n'; echo "${ENVIRONMENTS[*]}" )` - -Examples: - deploy staging example.com - deploy production example.com - deploy staging example.com -vv -T 60 -" -} - -[[ $# -lt 2 ]] && { show_usage; exit 0; } - -for arg -do - [[ $arg = -h ]] && { show_usage; exit 0; } -done - -ENV="$1"; shift -SITE="$1"; shift -EXTRA_PARAMS=$@ -DEPLOY_CMD="ansible-playbook deploy.yml -e env=$ENV -e site=$SITE $EXTRA_PARAMS" -HOSTS_FILE="hosts/$ENV" - -if [[ ! -e $HOSTS_FILE ]]; then - echo "Error: $ENV is not a valid environment ($HOSTS_FILE does not exist)." - echo - echo "Available environments:" - ( IFS=$'\n'; echo "${ENVIRONMENTS[*]}" ) - exit 0 -fi - -$DEPLOY_CMD diff --git a/bin/xdebug-tunnel.sh b/bin/xdebug-tunnel.sh deleted file mode 100755 index 8f475677ac..0000000000 --- a/bin/xdebug-tunnel.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -show_usage() { - echo " -Usage: bin/xdebug-tunnel.sh <action> <host> - -<action> can be 'open' or 'close' -<host> is the hostname, IP, or inventory alias in your \`hosts/<environment>\` file. - -Examples: - To open a tunnel: - bin/xdebug-tunnel.sh open 12.34.56.78 - - To close a tunnel: - bin/xdebug-tunnel.sh close 12.34.56.78 -" -} - -if [[ $1 == "open" ]]; then - REMOTE_ENABLE=1 -elif [[ $1 == "close" ]]; then - REMOTE_ENABLE=0 -else - >&2 echo "The provided <action> argument '${1}' is not acceptable." - show_usage - exit 1 -fi - -if [[ -z $2 ]]; then - >&2 echo "The <host> argument is required." - show_usage - exit 1 -fi - -XDEBUG_ENABLE="-e xdebug_remote_enable=${REMOTE_ENABLE}" -SSH_HOST="-e xdebug_tunnel_inventory_host=$2" - -if [[ -n $DEBUG ]]; then - PARAMS="$PARAMS ${VERBOSITY:--vvvv}" -fi - -ansible-playbook xdebug-tunnel.yml $XDEBUG_ENABLE $SSH_HOST $PARAMS diff --git a/deploy-hooks/build-after.yml b/deploy-hooks/build-after.yml new file mode 100644 index 0000000000..a06b4cae18 --- /dev/null +++ b/deploy-hooks/build-after.yml @@ -0,0 +1,12 @@ +# Placeholder `deploy_build_after` hook +# +# ⚠️ This example assumes your theme is using Sage 11 +# +# Uncomment the lines below if you are using Sage 11 +# NOTE: this task will fail if Sage theme is not activated at time of deployment. +# +# --- +# - name: Run Acorn optimize +# command: wp acorn optimize +# args: +# chdir: "{{ deploy_helper.new_release_path }}" diff --git a/deploy-hooks/build-before.yml b/deploy-hooks/build-before.yml index 44cce03693..2d246c6831 100644 --- a/deploy-hooks/build-before.yml +++ b/deploy-hooks/build-before.yml @@ -1,29 +1,43 @@ -# Placeholder `deploy_build_before` hook for building theme assets locally -# and then copying the files to the remote server +# Placeholder `deploy_build_before` hook for building theme assets on the +# host machine and then copying the files to the remote server # -# Uncomment the lines below and replace `sage` with your theme folder +# ⚠️ This example assumes your theme is using Sage 11 +# +# Uncomment the lines below if you are using Sage 11 +# and replace `sage` with your theme folder # # --- -# - name: Run yarn install -# command: yarn install -# connection: local +# - name: Install npm dependencies +# command: npm ci +# delegate_to: localhost # args: # chdir: "{{ project_local_path }}/web/app/themes/sage" # # - name: Install Composer dependencies -# command: composer install --no-ansi --no-dev --no-interaction --no-progress --optimize-autoloader --no-scripts +# command: composer install --no-ansi --no-dev --no-interaction --no-progress --optimize-autoloader --no-scripts --classmap-authoritative # args: # chdir: "{{ deploy_helper.new_release_path }}/web/app/themes/sage" # -# - name: Compile assets for production -# command: yarn run build:production -# connection: local +# - name: Compile assets +# command: npm run build +# delegate_to: localhost # args: # chdir: "{{ project_local_path }}/web/app/themes/sage" # -# - name: Copy project local files +# - name: Check for manifest +# stat: +# path: "{{ project_local_path }}/web/app/themes/sage/public/build/manifest.json" +# delegate_to: localhost +# register: entrypoints_data + +# - name: Entrypoints missing +# ansible.builtin.fail: +# msg: "The theme is missing the build manifest file" +# when: not entrypoints_data.stat.exists +# +# - name: Copy compiled assets # synchronize: -# src: "{{ project_local_path }}/web/app/themes/sage/dist" +# src: "{{ project_local_path }}/web/app/themes/sage/public" # dest: "{{ deploy_helper.new_release_path }}/web/app/themes/sage" # group: no # owner: no diff --git a/deploy.yml b/deploy.yml index 0962b3e156..cbe2d77de0 100644 --- a/deploy.yml +++ b/deploy.yml @@ -16,18 +16,18 @@ remote_user: "{{ web_user }}" pre_tasks: - name: Ensure site is valid - connection: local + delegate_to: localhost fail: msg: "Site `{{ site | default('') }}` is not valid. Available sites to deploy: {{ wordpress_sites.keys() | join(', ') }}" when: wordpress_sites[site | default('')] is not defined - name: Ensure repo is valid - connection: local + delegate_to: localhost fail: msg: | Invalid Git repository. Ensure that your site's `repo` variable is defined in `group_vars/{{ env }}/wordpress_sites.yml` and uses the SSH format (example: git@github.com:roots/bedrock.git) More info: - > https://roots.io/trellis/docs/deploys/ - when: project.repo is not defined or not project.repo | match(".*@.*:.*\.git") + > https://roots.io/trellis/docs/deployments/ + when: project.repo is not defined or project.repo is not match("^ssh://.+@.+|.+@.+:.+") roles: - deploy diff --git a/dev.yml b/dev.yml index 156e7fe0d4..4388cd55da 100644 --- a/dev.yml +++ b/dev.yml @@ -1,20 +1,21 @@ --- -- name: "WordPress Server: Install LEMP Stack with PHP 7.1 and MariaDB MySQL" +- name: "WordPress Server: Install LEMP Stack with PHP and MariaDB MySQL" hosts: web:&development become: yes - remote_user: vagrant roles: - { role: common, tags: [common] } - { role: fail2ban, tags: [fail2ban] } - { role: ferm, tags: [ferm] } + - { role: hosts, tags: [hosts] } - { role: ntp, tags: [ntp] } - { role: sshd, tags: [sshd] } - { role: mariadb, tags: [mariadb] } - - { role: mailhog, tags: [mailhog, mail] } + - { role: mailpit, tags: [mailpit, mailhog, mail] } - { role: php, tags: [php] } - { role: xdebug, tags: [php, xdebug] } - - { role: memcached, tags: [memcached] } + - { role: memcached, tags: [memcached], when: sites_using_memcached | count > 0 } + - { role: redis, tags: [redis], when: sites_using_redis | count > 0 } - { role: nginx, tags: [nginx] } - { role: logrotate, tags: [logrotate] } - { role: composer, tags: [composer] } diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000000..cd134b8748 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,21 @@ +--- +roles: + - name: composer + src: geerlingguy.composer + version: 1.9.2 + + - name: ntp + src: geerlingguy.ntp + version: 4.0.0 + + - name: logrotate + src: nickhammond.logrotate + version: v0.0.5 + + - name: swapfile + src: oefenweb.swapfile + version: v2.0.42 + + - name: mailpit + src: roots.mailpit + version: v1.0.0 diff --git a/group_vars/all/helpers.yml b/group_vars/all/helpers.yml index d0ab088e3c..28bc5202c7 100644 --- a/group_vars/all/helpers.yml +++ b/group_vars/all/helpers.yml @@ -8,12 +8,41 @@ wordpress_env_defaults: wp_home: "{{ ssl_enabled | ternary('https', 'http') }}://{{ site_hosts_canonical | first }}" wp_siteurl: "{{ ssl_enabled | ternary('https', 'http') }}://{{ site_hosts_canonical | first }}/wp" domain_current_site: "{{ site_hosts_canonical | first }}" + wp_debug_log: "{{ www_root }}/{{ item.key }}/logs/debug.log" -site_env: "{{ wordpress_env_defaults | combine(item.value.env | default({}), vault_wordpress_sites[item.key].env) }}" +# Redis cache environment variables (when enabled) +redis_cache_env: + wp_redis_host: "{{ item.value.object_cache.host | default('127.0.0.1') }}" + wp_redis_port: "{{ item.value.object_cache.port | default(6379) }}" + wp_redis_database: "{{ item.value.object_cache.database | default(0) }}" + wp_redis_prefix: "{{ item.value.object_cache.prefix | default(item.key | underscore + '_') }}" + wp_cache_key_salt: "{{ item.key }}_{{ env }}" + +# Memcached cache environment variables (when enabled) +memcached_cache_env: + wp_memcached_host: "{{ item.value.object_cache.host | default('127.0.0.1') }}" + wp_memcached_port: "{{ item.value.object_cache.port | default(11211) }}" + wp_memcached_prefix: "{{ item.value.object_cache.prefix | default(item.key | underscore + '_') }}" + wp_cache_key_salt: "{{ item.key }}_{{ env }}" + +site_env: "{{ wordpress_env_defaults | combine(vault_wordpress_env_defaults | default({}), object_cache_enabled_redis | ternary(redis_cache_env, {}), object_cache_enabled_memcached | ternary(memcached_cache_env, {}), item.value.env | default({}), vault_wordpress_sites[item.key].env) }}" + +object_cache_enabled_redis: "{{ item.value.object_cache.enabled | default(false) and item.value.object_cache.provider | default('') == 'redis' }}" +object_cache_enabled_memcached: "{{ item.value.object_cache.enabled | default(false) and item.value.object_cache.provider | default('') == 'memcached' }}" + +# Sites using Redis or Memcached object cache +sites_using_redis: "{{ (wordpress_sites | select_sites('object_cache.enabled', 'true') | select_sites('object_cache.provider', 'eq', 'redis')).keys() | list }}" +sites_using_memcached: "{{ (wordpress_sites | select_sites('object_cache.enabled', 'true') | select_sites('object_cache.provider', 'eq', 'memcached')).keys() | list }}" site_hosts_canonical: "{{ item.value.site_hosts | map(attribute='canonical') | list }}" site_hosts_redirects: "{{ item.value.site_hosts | selectattr('redirects', 'defined') | sum(attribute='redirects', start=[]) | list }}" site_hosts: "{{ site_hosts_canonical | union(site_hosts_redirects) }}" multisite_subdomains_wildcards: "{{ item.value.multisite.subdomains | default(false) | ternary( site_hosts_canonical | map('regex_replace', '^(www\\.)?(.*)$', '*.\\2') | list, [] ) }}" ssl_enabled: "{{ item.value.ssl is defined and item.value.ssl.enabled | default(false) }}" -ssl_stapling_enabled: "{{ item.value.ssl is defined and item.value.ssl.stapling_enabled | default(true) }}" cron_enabled: "{{ site_env.disable_wp_cron and (not item.value.multisite.enabled | default(false) or (item.value.multisite.enabled | default(false) and item.value.multisite.cron | default(true))) }}" +sites_use_ssl: "{{ (wordpress_sites | select_sites('ssl.enabled', 'true') | length) > 0 }}" + +composer_authentications: "{{ vault_wordpress_sites[site].composer_authentications | default([]) }}" +# Default `type` is `http-basic`. +composer_authentications_using_basic_auth: "{{ composer_authentications | rejectattr('type', 'defined') | union( composer_authentications | selectattr('type', 'defined') | selectattr('type', 'equalto', 'http-basic') ) }}" +composer_authentications_using_bitbucket_oauth: "{{ composer_authentications | selectattr('type', 'defined') | selectattr('type', 'equalto', 'bitbucket-oauth') }}" +composer_authentications_using_other_token: "{{ composer_authentications | selectattr('type', 'defined') | rejectattr('type', 'equalto', 'http-basic') | rejectattr('type', 'equalto', 'bitbucket-oauth') }}" diff --git a/group_vars/all/known_hosts.yml b/group_vars/all/known_hosts.yml index b8af937ca5..e337816980 100644 --- a/group_vars/all/known_hosts.yml +++ b/group_vars/all/known_hosts.yml @@ -6,8 +6,12 @@ known_hosts: - name: github.com key: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== + - name: github.com + key: github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl + - name: bitbucket.org + key: bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M= - name: bitbucket.org - key: bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw== + key: bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO - name: gitlab.com key: gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf - name: gitlab.com diff --git a/group_vars/all/logrotate.yml b/group_vars/all/logrotate.yml index 438947254c..ba1ba124fa 100644 --- a/group_vars/all/logrotate.yml +++ b/group_vars/all/logrotate.yml @@ -1,4 +1,4 @@ -# Documentation: https://galaxy.ansible.com/list#/roles/1117 +# Documentation: https://github.com/nickhammond/ansible-logrotate logrotate_scripts: - name: wordpress-sites path: "{{ www_root }}/**/logs/*.log" @@ -17,4 +17,4 @@ logrotate_scripts: if [ -d /etc/logrotate.d/httpd-prerotate ]; then \ run-parts /etc/logrotate.d/httpd-prerotate; \ fi \ - postrotate: service nginx rotate + postrotate: service nginx reload >/dev/null 2>&1 diff --git a/group_vars/all/main.yml b/group_vars/all/main.yml index bcc0f88483..c0df7453f4 100644 --- a/group_vars/all/main.yml +++ b/group_vars/all/main.yml @@ -1,12 +1,14 @@ -composer_keep_updated: true -composer_global_packages: - - { name: hirak/prestissimo } apt_cache_valid_time: 3600 +apt_package_state: present +apt_security_package_state: latest +apt_dev_package_state: latest +apt_clean_sources: false +composer_keep_updated: true +php_version: "8.3" ntp_timezone: Etc/UTC ntp_manage_config: true www_root: /srv/www -ip_whitelist: - - "{{ (env == 'development') | ternary(ansible_default_ipv4.gateway, ipify_public_ip | default('')) }}" +max_journal_size: 512M # Values of raw_vars will be wrapped in `{% raw %}` to avoid templating problems if values include `{%` and `{{`. # Will recurse dicts/lists. `*` is wildcard for one or more dict keys, list indices, or strings. Example: @@ -17,4 +19,5 @@ raw_vars: - vault_mysql_root_password - vault_users.*.password - vault_users.*.salt + - vault_wordpress_env_defaults - vault_wordpress_sites diff --git a/group_vars/all/security.yml b/group_vars/all/security.yml index 2d9df3d3b9..57c2bcdd7d 100644 --- a/group_vars/all/security.yml +++ b/group_vars/all/security.yml @@ -1,7 +1,13 @@ +# Documentation: https://roots.io/trellis/docs/security/ + ferm_input_list: - type: dport_accept - dport: [http, https] + dport: [http] filename: nginx_accept + - type: dport_accept + dport: [https] + filename: nginx_accept_https + delete: "{{ not (sites_use_ssl | bool) }}" - type: dport_accept dport: [ssh] saddr: "{{ ip_whitelist }}" @@ -10,8 +16,25 @@ ferm_input_list: seconds: 300 hits: 20 -# Documentation: https://roots.io/trellis/docs/security/ + +# Enable built-in fail2ban services or add your own custom ones +fail2ban_services_custom: + - name: wordpress_xmlrpc + filter: wordpress-xmlrpc + enabled: "false" + port: http,https + logpath: "{{ www_root }}/**/logs/access.log" + - name: wordpress_wp_login + filter: wordpress-wp-login + enabled: "false" + port: http,https + logpath: "{{ www_root }}/**/logs/access.log" + # If sshd_permit_root_login: false, admin_user must be in 'users' (`group_vars/all/users.yml`) with sudo group # and in 'vault_users' (`group_vars/staging/vault.yml`, `group_vars/production/vault.yml`) sshd_permit_root_login: true sshd_password_authentication: false + +ip_whitelist: + - 127.0.0.0/8 + - "{{ ipify_public_ip | default('') }}" diff --git a/group_vars/all/users.yml b/group_vars/all/users.yml index e2d615012f..4d38ac23d2 100644 --- a/group_vars/all/users.yml +++ b/group_vars/all/users.yml @@ -7,16 +7,18 @@ users: groups: - "{{ web_group }}" keys: - - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + - "{{ lookup('file', '~/.ssh/id_rsa.pub', errors='ignore') }}" + - "{{ lookup('file', '~/.ssh/id_ed25519.pub', errors='ignore') }}" # - https://github.com/username.keys - name: "{{ admin_user }}" groups: - sudo keys: - - "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + - "{{ lookup('file', '~/.ssh/id_rsa.pub', errors='ignore') }}" + - "{{ lookup('file', '~/.ssh/id_ed25519.pub', errors='ignore') }}" # - https://github.com/username.keys web_user: web web_group: www-data web_sudoers: - - "/usr/sbin/service php7.1-fpm *" + - "/usr/sbin/service php{{ php_version }}-fpm *" diff --git a/group_vars/all/vault.yml b/group_vars/all/vault.yml index 19a21ae41e..d46f2cc6f4 100644 --- a/group_vars/all/vault.yml +++ b/group_vars/all/vault.yml @@ -1,2 +1,8 @@ # Documentation: https://roots.io/trellis/docs/vault/ vault_mail_password: smtp_password + +# Variables to accompany `wordpress_env_defaults` in `group_vars/all/helpers.yml` +# Note: These values can be overridden by `vault_wordpress_sites.*.env` +# +# vault_wordpress_env_defaults: +# my_api_key: 'available to all environments' diff --git a/group_vars/development/mail.yml b/group_vars/development/mail.yml index f6d4f663d5..acdcc7880b 100644 --- a/group_vars/development/mail.yml +++ b/group_vars/development/mail.yml @@ -1,2 +1,2 @@ # Documentation: https://roots.io/trellis/docs/mail/ -php_sendmail_path: "{{ mailhog_install_dir }}/mhsendmail" +php_sendmail_path: "{{ mailpit_install_dir }}/mailpit sendmail" diff --git a/group_vars/development/main.yml b/group_vars/development/main.yml index 8d7c15913c..2723db8565 100644 --- a/group_vars/development/main.yml +++ b/group_vars/development/main.yml @@ -1,5 +1,4 @@ acme_tiny_challenges_directory: "{{ www_root }}/letsencrypt" env: development -ferm_enabled: false mysql_root_password: "{{ vault_mysql_root_password }}" # Define this variable in group_vars/development/vault.yml -web_user: vagrant +web_user: "{{ ansible_user | default ('web') }}" diff --git a/group_vars/development/php.yml b/group_vars/development/php.yml index 7b9af47888..cda1a39022 100644 --- a/group_vars/development/php.yml +++ b/group_vars/development/php.yml @@ -1,9 +1,9 @@ php_error_reporting: 'E_ALL' php_display_errors: 'On' php_display_startup_errors: 'On' -php_track_errors: 'On' php_mysqlnd_collect_memory_statistics: 'On' -php_opcache_enable: 0 +php_opcache_revalidate_freq: 0 -xdebug_remote_enable: 1 -xdebug_remote_connect_back: 1 +xdebug_mode: 'debug' +xdebug_start_with_request: 'yes' +xdebug_discover_client_host: 1 diff --git a/group_vars/development/security.yml b/group_vars/development/security.yml new file mode 100644 index 0000000000..fe079a2f7e --- /dev/null +++ b/group_vars/development/security.yml @@ -0,0 +1,6 @@ +ferm_enabled: false +ip_whitelist: + - 127.0.0.0/8 + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 diff --git a/group_vars/development/wordpress_sites.yml b/group_vars/development/wordpress_sites.yml index 5153ccc612..fc77199f30 100644 --- a/group_vars/development/wordpress_sites.yml +++ b/group_vars/development/wordpress_sites.yml @@ -1,15 +1,15 @@ -# Documentation: https://roots.io/trellis/docs/local-development-setup/ -# `wordpress_sites` options: https://roots.io/trellis/docs/wordpress-sites +# Documentation: https://roots.io/trellis/docs/local-development/ +# `wordpress_sites` options: https://roots.io/trellis/docs/wordpress-sites/ # Define accompanying passwords/secrets in group_vars/development/vault.yml wordpress_sites: example.com: site_hosts: - - canonical: example.dev + - canonical: example.test redirects: - - www.example.dev + - www.example.test local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) - admin_email: admin@example.dev + admin_email: admin@example.test multisite: enabled: false ssl: @@ -17,3 +17,6 @@ wordpress_sites: provider: self-signed cache: enabled: false + xmlrpc: + enabled: false + diff --git a/group_vars/production/wordpress_sites.yml b/group_vars/production/wordpress_sites.yml index e8a875d1ca..168b36a385 100644 --- a/group_vars/production/wordpress_sites.yml +++ b/group_vars/production/wordpress_sites.yml @@ -11,7 +11,7 @@ wordpress_sites: local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) repo: git@github.com:example/example.com.git # replace with your Git repo URL repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo - branch: master + branch: main multisite: enabled: false ssl: @@ -19,3 +19,5 @@ wordpress_sites: provider: letsencrypt cache: enabled: false + xmlrpc: + enabled: false diff --git a/group_vars/staging/wordpress_sites.yml b/group_vars/staging/wordpress_sites.yml index 054770ea7a..36d83199f2 100644 --- a/group_vars/staging/wordpress_sites.yml +++ b/group_vars/staging/wordpress_sites.yml @@ -11,7 +11,7 @@ wordpress_sites: local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) repo: git@github.com:example/example.com.git # replace with your Git repo URL repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo - branch: master + branch: main multisite: enabled: false ssl: @@ -19,3 +19,5 @@ wordpress_sites: provider: letsencrypt cache: enabled: false + xmlrpc: + enabled: false diff --git a/hosts/development b/hosts/development index 4af294aca7..694b39e540 100644 --- a/hosts/development +++ b/hosts/development @@ -1,41 +1,8 @@ -# This file is only used for Windows hosts. -# -# Windows -# ------------------------------------------------------------- -# If you want to run `dev.yml` manually you can SSH into the VM -# to the directory with the `dev.yml` playbook and run: - -# `ansible-playbook dev.yml` -# -# Non-Windows -# ------------------------------------------------------------- -# If you want to run `dev.yml` manually via the `ansible-playbook` -# command (vs. `vagrant up` or `vagrant provision`), you might be -# inclined to define your development host information in this file. -# We recommend instead that you use the `-i` (inventory) option with -# your `ansible-playbook` command to specify the custom inventory file -# Vagrant has created for the VM. Vagrant's custom inventory -# includes necessary non-standard SSH connection information. -# -# Here is an example command: -# -# `ansible-playbook dev.yml -i .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory` -# -# The `.vagrant` directory above is usually in the same directory as -# your `Vagrantfile`. If not, you will need to adjust this path in the -# command. -# -# Why run `dev.yml` manually? -# ------------------------------------------------------------- -# One reason you may want to run `dev.yml` via the `ansible-playbook` -# command is for the convenience of adding Ansible options via the -# command line (e.g., `--tags`, `--skip-tags`, or `-vvvv`). In contrast, -# the commands `vagrant up` and `vagrant provision` would only run the -# `dev.yml` playbook with such options if you were edit the options -# into the Vagrantfile's `config.vm.provision` section. +# Add each host to the [development] group and to a "type" group such as [web] or [db]. +# List each machine only once per [group], even if it will host multiple sites. [development] -192.168.50.5 ansible_connection=local +192.168.56.5 [web] -192.168.50.5 ansible_connection=local +192.168.56.5 diff --git a/lib/trellis/__init__.py b/lib/trellis/__init__.py deleted file mode 100644 index 980f84a225..0000000000 --- a/lib/trellis/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type diff --git a/lib/trellis/config.rb b/lib/trellis/config.rb deleted file mode 100644 index 5dae571c70..0000000000 --- a/lib/trellis/config.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -require 'vagrant' -require 'yaml' - -module Trellis - class Config - def initialize(root_path:) - @root_path = root_path - end - - def multisite_subdomains? - @using_multisite_subdomains ||= begin - wordpress_sites.any? do |(_name, site)| - next false unless multisite = site['multisite'] - multisite.fetch('enabled', false) && multisite.fetch('subdomains', false) - end - end - end - - def site_hosts_canonical - @site_hosts_canonical ||= site_hosts.map { |host| host['canonical'] } - end - - def site_hosts_redirects - @site_hosts_redirects ||= site_hosts.flat_map { |host| host['redirects'] }.compact - end - - def site_hosts - @site_hosts ||= begin - wordpress_sites.flat_map { |(_name, site)| site['site_hosts'] }.tap do |hosts| - fail_with message: template_content if malformed?(site_hosts: hosts) - end - end - end - - def wordpress_sites - @wordpress_sites ||= begin - content['wordpress_sites'].tap do |sites| - fail_with message: "No sites found in #{path}." if sites.to_h.empty? - end - end - end - - def content - @content ||= begin - fail_with message: "#{path} was not found. Please check `root_path`." unless exist? - YAML.load_file(path) - end - end - - private - - def malformed?(site_hosts:) - site_hosts.any? do |host| - !host.is_a?(Hash) || !host.key?('canonical') - end - end - - def exist? - File.exist?(path) - end - - def path - File.join(@root_path, 'group_vars', 'development', 'wordpress_sites.yml') - end - - def template_content - File.read(File.join(@root_path, 'roles', 'common', 'templates', 'site_hosts.j2')).sub!('{{ env }}', 'development').gsub!(/com$/, 'dev') - end - - def fail_with(message:) - raise Vagrant::Errors::VagrantError.new, message - end - end -end diff --git a/lib/trellis/plugins/callback/output.py b/lib/trellis/plugins/callback/output.py deleted file mode 100644 index 9bb2cbede6..0000000000 --- a/lib/trellis/plugins/callback/output.py +++ /dev/null @@ -1,79 +0,0 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os.path -import sys - -from ansible.plugins.callback.default import CallbackModule as CallbackModule_default - -try: - from trellis.utils import output as output -except ImportError: - ansible_path = os.getenv('ANSIBLE_CONFIG', os.getcwd()) - if sys.path.append(os.path.join(ansible_path, 'lib')) in sys.path: raise - sys.path.append(sys.path.append(os.path.join(ansible_path, 'lib'))) - from trellis.utils import output as output - - -class CallbackModule(CallbackModule_default): - ''' Customizes the default Ansible output ''' - - CALLBACK_VERSION = 2.0 - CALLBACK_TYPE = 'stdout' - CALLBACK_NAME = 'output' - - def __init__(self): - super(CallbackModule, self).__init__() - output.reset_task_info(self) - self.vagrant_version = None - - def v2_runner_on_failed(self, result, ignore_errors=False): - self.task_failed = True - output.display_host(self, result) - super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors) - - def v2_runner_on_ok(self, result): - output.display_host(self, result) - super(CallbackModule, self).v2_runner_on_ok(result) - - def v2_runner_on_skipped(self, result): - output.display_host(self, result) - super(CallbackModule, self).v2_runner_on_skipped(result) - - def v2_runner_on_unreachable(self, result): - self.task_failed = True - output.display_host(self, result) - super(CallbackModule, self).v2_runner_on_unreachable(result) - - def v2_playbook_on_task_start(self, task, is_conditional): - output.reset_task_info(self, task) - super(CallbackModule, self).v2_playbook_on_task_start(task, is_conditional) - - def v2_playbook_on_handler_task_start(self, task): - output.reset_task_info(self, task) - super(CallbackModule, self).v2_playbook_on_handler_task_start(task) - - def v2_playbook_on_play_start(self, play): - super(CallbackModule, self).v2_playbook_on_play_start(play) - - # Check for relevant settings or overrides passed via cli --extra-vars - extra_vars = play.get_variable_manager().extra_vars - if 'vagrant_version' in extra_vars: - self.vagrant_version = extra_vars['vagrant_version'] - - def v2_runner_item_on_ok(self, result): - output.display_item(self, result) - output.replace_item_with_key(self, result) - super(CallbackModule, self).v2_runner_item_on_ok(result) - - def v2_runner_item_on_failed(self, result): - self.task_failed = True - output.display_item(self, result) - output.replace_item_with_key(self, result) - super(CallbackModule, self).v2_runner_item_on_failed(result) - - def v2_runner_item_on_skipped(self, result): - output.display_item(self, result) - output.replace_item_with_key(self, result) - super(CallbackModule, self).v2_runner_item_on_skipped(result) diff --git a/lib/trellis/plugins/callback/vars.py b/lib/trellis/plugins/callback/vars.py index 42942bdca1..544df440bb 100644 --- a/lib/trellis/plugins/callback/vars.py +++ b/lib/trellis/plugins/callback/vars.py @@ -1,17 +1,15 @@ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - import re import sys +import os -from __main__ import cli from ansible.module_utils.six import iteritems from ansible.errors import AnsibleError -from ansible.parsing.dataloader import DataLoader from ansible.parsing.yaml.objects import AnsibleMapping, AnsibleSequence, AnsibleUnicode from ansible.playbook.play_context import PlayContext from ansible.plugins.callback import CallbackBase from ansible.template import Templar +from ansible.utils.unsafe_proxy import wrap_var +from ansible import context class CallbackModule(CallbackBase): @@ -21,13 +19,13 @@ class CallbackModule(CallbackBase): CALLBACK_NAME = 'vars' def __init__(self): - self.loader = DataLoader() - self._options = cli.options if cli else None + super(CallbackModule, self).__init__() + self._options = context.CLIARGS def raw_triage(self, key_string, item, patterns): # process dict values if isinstance(item, AnsibleMapping): - return AnsibleMapping(dict((key,self.raw_triage('.'.join([key_string, key]), value, patterns)) for key,value in item.iteritems())) + return AnsibleMapping(dict((key,self.raw_triage('.'.join([key_string, key]), value, patterns)) for key,value in iteritems(item))) # process list values elif isinstance(item, AnsibleSequence): @@ -36,18 +34,21 @@ def raw_triage(self, key_string, item, patterns): # wrap values if they match raw_vars pattern elif isinstance(item, AnsibleUnicode): match = next((pattern for pattern in patterns if re.match(pattern, key_string)), None) - return AnsibleUnicode(''.join(['{% raw %}', item, '{% endraw %}'])) if not item.startswith(('{% raw', '{%raw')) and match else item + return wrap_var(item) if match else item + + else: + return item def raw_vars(self, play, host, hostvars): if 'raw_vars' not in hostvars: return - raw_vars = Templar(variables=hostvars, loader=self.loader).template(hostvars['raw_vars']) + raw_vars = Templar(variables=hostvars, loader=play._loader).template(hostvars['raw_vars']) if not isinstance(raw_vars, list): raise AnsibleError('The `raw_vars` variable must be defined as a list.') - patterns = [re.sub(r'\*', '(.)*', re.sub(r'\.', '\.', var)) for var in raw_vars if var.split('.')[0] in hostvars] - keys = set(pattern.split('\.')[0] for pattern in patterns) + patterns = [re.sub(r'\*', '(.)*', re.sub(r'\.', r'\.', var)) for var in raw_vars if var.split('.')[0] in hostvars] + keys = set(pattern.split(r'\.')[0] for pattern in patterns) for key in keys: if key in play.vars: play.vars[key] = self.raw_triage(key, play.vars[key], patterns) @@ -66,34 +67,29 @@ def cli_options(self): '--vault-password-file': 'vault_password_file', } - for option,value in strings.iteritems(): - if getattr(self._options, value, False): - options.append("{0}='{1}'".format(option, str(getattr(self._options, value)))) + for option,value in iteritems(strings): + if self._options.get(value, False): + options.append("{0}='{1}'".format(option, str(self._options.get(value)))) - for inventory in getattr(self._options, 'inventory'): + for inventory in self._options.get('inventory'): options.append("--inventory='{}'".format(str(inventory))) - if getattr(self._options, 'ask_vault_pass', False): + if self._options.get('ask_vault_pass', False): options.append('--ask-vault-pass') return ' '.join(options) - def darwin_without_passlib(self): - if not sys.platform.startswith('darwin'): - return False + def v2_playbook_on_play_start(self, play): + play_context = PlayContext(play=play) - try: - import passlib.hash - return False - except: - return True + env = play.get_variable_manager().get_vars(play=play).get('env', '') + env_group = next((group for key,group in iteritems(play.get_variable_manager()._inventory.groups) if key == env), False) + if env_group: + env_group.set_priority(20) - def v2_playbook_on_play_start(self, play): for host in play.get_variable_manager()._inventory.list_hosts(play.hosts[0]): hostvars = play.get_variable_manager().get_vars(play=play, host=host) self.raw_vars(play, host, hostvars) - host.vars['ssh_args_default'] = PlayContext(play=play, options=self._options)._ssh_args.default host.vars['cli_options'] = self.cli_options() - host.vars['cli_ask_pass'] = getattr(self._options, 'ask_pass', False) - host.vars['cli_ask_become_pass'] = getattr(self._options, 'become_ask_pass', False) - host.vars['darwin_without_passlib'] = self.darwin_without_passlib() + host.vars['cli_ask_pass'] = self._options.get('ask_pass', False) + host.vars['cli_ask_become_pass'] = self._options.get('become_ask_pass', False) diff --git a/lib/trellis/plugins/filter/filters.py b/lib/trellis/plugins/filter/filters.py index 6c8eed5eb7..9480446a8e 100644 --- a/lib/trellis/plugins/filter/filters.py +++ b/lib/trellis/plugins/filter/filters.py @@ -1,25 +1,61 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function, unicode_literals) -__metaclass__ = type - import types from ansible import errors from ansible.module_utils.six import string_types +from jinja2 import pass_environment def to_env(dict_value): - envs = ["{0}='{1}'".format(key.upper(), str(value).replace("'","\\'")) for key, value in sorted(dict_value.items())] + envs = ['{0}="{1}"'.format(key.upper(), str(value).replace('"', '\\"')) for key, value in sorted(dict_value.items())] return "\n".join(envs) def underscore(value): ''' Convert dots to underscore in a string ''' return value.replace('.', '_') -class FilterModule(object): - ''' Trellis jinja2 filters ''' +def get_nested_attr(data, attr_path): + """Helper to safely get a nested attribute from a dict.""" + keys = attr_path.split('.') + for key in keys: + if not isinstance(data, dict) or key not in data: + return None + data = data[key] + return data + +@pass_environment +def select_sites(env, sites, attr_path, test_name='defined', *args): + """ + A filter that mimics selectattr but works on nested attributes safely. + It uses Jinja's own built-in tests. + """ + test_func = env.tests.get(test_name) + if test_func is None: + raise Exception(f"Unknown Jinja2 test '{test_name}'") + + if not isinstance(sites, dict): + return {} + + result = {} + for name, site_data in sites.items(): + value_to_test = get_nested_attr(site_data, attr_path) + # For most tests, we skip sites where the attribute doesn't exist. + if value_to_test is None and test_name != 'defined': + continue + + # Handle tests that don't take arguments, like 'true' or 'false' + if not args and test_name in ['true', 'false', 'undefined', 'defined']: + if test_func(value_to_test): + result[name] = site_data + # Handle tests that do take arguments + elif test_func(value_to_test, *args): + result[name] = site_data + + return result + +class FilterModule(object): def filters(self): return { + 'select_sites': select_sites, 'to_env': to_env, 'underscore': underscore, } diff --git a/lib/trellis/plugins/vars/version.py b/lib/trellis/plugins/vars/version.py index fd6c7b4d4c..c18ad1582c 100644 --- a/lib/trellis/plugins/vars/version.py +++ b/lib/trellis/plugins/vars/version.py @@ -1,11 +1,7 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - from ansible import __version__ from ansible.errors import AnsibleError -from distutils.version import LooseVersion -from operator import ge, gt +from packaging.version import Version +from platform import python_version, python_version_tuple try: from __main__ import display @@ -13,14 +9,19 @@ from ansible.utils.display import Display display = Display() -version_requirement = '2.4.0.0' -version_tested_max = '2.4.0.0' +version_requirement = '2.10.0' +version_tested_max = '5.4.0' + +if python_version_tuple()[0] == '2': + raise AnsibleError(('Trellis no longer supports Python 2 (you are using version {}).' + ' Python 2 reached end of life in 2020 and is unmaintained.\n' + 'Python 3 is required as of Trellis version v1.15.0.').format(python_version())) -if not ge(LooseVersion(__version__), LooseVersion(version_requirement)): +if Version(__version__) < Version(version_requirement): raise AnsibleError(('Trellis no longer supports Ansible {}.\n' 'Please upgrade to Ansible {} or higher.').format(__version__, version_requirement)) -elif gt(LooseVersion(__version__), LooseVersion(version_tested_max)): - display.warning(u'You Ansible version is {} but this version of Trellis has only been tested for ' +elif Version(__version__) > Version(version_tested_max): + display.warning(u'Your Ansible version is {} but this version of Trellis has only been tested for ' u'compatability with Ansible {} -> {}. It is advisable to check for Trellis updates or ' u'downgrade your Ansible version.'.format(__version__, version_requirement, version_tested_max)) diff --git a/lib/trellis/utils/__init__.py b/lib/trellis/utils/__init__.py deleted file mode 100644 index 980f84a225..0000000000 --- a/lib/trellis/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type diff --git a/lib/trellis/utils/output.py b/lib/trellis/utils/output.py deleted file mode 100644 index 7cd54ba5c8..0000000000 --- a/lib/trellis/utils/output.py +++ /dev/null @@ -1,123 +0,0 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os.path -import platform -import re -import textwrap - -from ansible import __version__ - -# to_unicode will no longer be needed once Trellis requires Ansible >= 2.2 -try: - from ansible.module_utils._text import to_text -except ImportError: - from ansible.utils.unicode import to_unicode as to_text - -def system(vagrant_version=None): - # Get most recent Trellis CHANGELOG entry - changelog_msg = '' - ansible_path = os.getenv('ANSIBLE_CONFIG', os.getcwd()) - changelog = os.path.join(ansible_path, 'CHANGELOG.md') - - if os.path.isfile(changelog): - with open(changelog) as f: - str = f.read(200) - - # Retrieve release number if it is most recent entry - release = re.search(r'^###\s((?!HEAD).*)', str) - if release is not None: - changelog_msg = '\n Trellis {0}'.format(release.group(1)) - - # Retrieve most recent changelog entry - else: - change = re.search(r'^\*\s?(\[BREAKING\])?([^\(\n\[]+)', str, re.M|re.I) - if change is not None: - changelog_msg = '\n Trellis at "{0}"'.format(change.group(2).strip()) - - # Vagrant info, if available - vagrant = ' Vagrant {0};'.format(vagrant_version) if vagrant_version else '' - - # Assemble components and return - return 'System info:\n Ansible {0};{1} {2}{3}'.format(__version__, vagrant, platform.system(), changelog_msg) - -def reset_task_info(obj, task=None): - obj.action = None if task is None else task._get_parent_attribute('action') - obj.first_host = True - obj.first_item = True - obj.task_failed = False - -# Display dict key only, instead of full json dump -def replace_item_with_key(obj, result): - if not obj._display.verbosity and 'label' not in result._task._ds.get('loop_control', {}): - item = '_ansible_item_label' if '_ansible_item_label' in result._result else 'item' - if 'key' in result._result[item]: - result._result[item] = result._result[item]['key'] - elif type(result._result[item]) is dict: - subitem = '_ansible_item_label' if '_ansible_item_label' in result._result[item] else 'item' - if 'key' in result._result[item].get(subitem, {}): - result._result[item] = result._result[item][subitem]['key'] - elif '_ansible_item_label' in result._result[item]: - result._result[item] = result._result[item]['_ansible_item_label'] - -def display(obj, result): - msg = '' - result = result._result - display = obj._display.display - wrap_width = 77 - first = obj.first_host and obj.first_item - failed = result.get('failed', False) or result.get('unreachable', False) - - # Only display msg if debug module or if failed (some modules have undesired 'msg' on 'ok') - if 'msg' in result and (failed or obj.action == 'debug'): - msg = result.pop('msg', '') - - # Disable Ansible's verbose setting for debug module to avoid the CallbackBase._dump_results() - if '_ansible_verbose_always' in result: - del result['_ansible_verbose_always'] - - # Display additional info when failed - if failed: - items = (item for item in ['reason', 'module_stderr', 'module_stdout', 'stderr'] if item in result and to_text(result[item]) != '') - for item in items: - msg = result[item] if msg == '' else '\n'.join([msg, result.pop(item, '')]) - - # Add blank line between this fail message and the json dump Ansible displays next - msg = '\n'.join([msg, '']) - - # Must pass unicode strings to Display.display() to prevent UnicodeError tracebacks - if isinstance(msg, list): - msg = '\n'.join([to_text(x) for x in msg]) - elif not isinstance(msg, unicode): - msg = to_text(msg) - - # Wrap text - msg = '\n'.join([textwrap.fill(line, wrap_width, replace_whitespace=False) - for line in msg.splitlines()]) - - # Display system info and msg, with horizontal rule between hosts/items - hr = '-' * int(wrap_width*.67) - - if obj.task_failed and first: - display(system(obj.vagrant_version), 'bright gray') - display(hr, 'bright gray') - - if msg == '': - if obj.task_failed and not first: - display(hr, 'bright gray') - else: - return - else: - if not first: - display(hr, 'bright gray') - display(msg, 'red' if failed else 'bright purple') - -def display_host(obj, result): - if 'results' not in result._result: - display(obj, result) - obj.first_host = False - -def display_item(obj, result): - display(obj, result) - obj.first_item = False diff --git a/lib/trellis/vagrant.rb b/lib/trellis/vagrant.rb deleted file mode 100644 index 16cf2f3dd4..0000000000 --- a/lib/trellis/vagrant.rb +++ /dev/null @@ -1,78 +0,0 @@ -# Set Ansible paths relative to Ansible directory -ENV['ANSIBLE_CONFIG'] = ANSIBLE_PATH -ENV['ANSIBLE_CALLBACK_PLUGINS'] = "~/.ansible/plugins/callback_plugins/:/usr/share/ansible_plugins/callback_plugins:#{File.join(ANSIBLE_PATH, 'lib/trellis/plugins/callback')}" -ENV['ANSIBLE_FILTER_PLUGINS'] = "~/.ansible/plugins/filter_plugins/:/usr/share/ansible_plugins/filter_plugins:#{File.join(ANSIBLE_PATH, 'lib/trellis/plugins/filter')}" -ENV['ANSIBLE_LIBRARY'] = "/usr/share/ansible:#{File.join(ANSIBLE_PATH, 'lib/trellis/modules')}" -ENV['ANSIBLE_ROLES_PATH'] = File.join(ANSIBLE_PATH, 'vendor', 'roles') -ENV['ANSIBLE_VARS_PLUGINS'] = "~/.ansible/plugins/vars_plugins/:/usr/share/ansible_plugins/vars_plugins:#{File.join(ANSIBLE_PATH, 'lib/trellis/plugins/vars')}" - -def ensure_plugins(plugins) - logger = Vagrant::UI::Colored.new - installed = false - - plugins.each do |plugin| - plugin_name = plugin['name'] - manager = Vagrant::Plugin::Manager.instance - - next if manager.installed_plugins.has_key?(plugin_name) - - logger.warn("Installing plugin #{plugin_name}") - - manager.install_plugin( - plugin_name, - sources: plugin.fetch('source', %w(https://rubygems.org/ https://gems.hashicorp.com/)), - version: plugin['version'] - ) - - installed = true - end - - if installed - logger.warn('`vagrant up` must be re-run now that plugins are installed') - exit - end -end - -def fail_with_message(msg) - fail Vagrant::Errors::VagrantError.new, msg -end - -def local_provisioning? - @local_provisioning ||= Vagrant::Util::Platform.windows? || !which('ansible-playbook') || ENV['FORCE_ANSIBLE_LOCAL'] -end - -def local_site_path(site) - File.expand_path(site['local_path'], ANSIBLE_PATH) -end - -def nfs_path(path) - "/vagrant-nfs-#{File.basename(path)}" -end - -def post_up_message - msg = 'Your Trellis Vagrant box is ready to use!' - msg << "\n* Composer and WP-CLI commands need to be run on the virtual machine" - msg << "\n for any post-provision modifications." - msg << "\n* You can SSH into the machine with `vagrant ssh`." - msg << "\n* Then navigate to your WordPress sites at `/srv/www`" - msg << "\n or to your Trellis files at `#{ANSIBLE_PATH_ON_VM}`." - - msg -end - -def remote_site_path(site_name, site) - "/srv/www/#{site_name}/#{site['current_path'] || 'current'}" -end - -def which(cmd) - exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] - - paths = ENV['PATH'].split(File::PATH_SEPARATOR).flat_map do |path| - exts.map { |ext| File.join(path, "#{cmd}#{ext}") } - end - - paths.any? do |path| - next unless File.executable?(path) && !File.directory?(path) - system("#{path} --help", %i(out err) => File::NULL) - end -end diff --git a/public_keys/.gitkeep b/public_keys/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..3d111fca09 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +ansible>=2.10.0 diff --git a/requirements.yml b/requirements.yml deleted file mode 100644 index a5dbaf621d..0000000000 --- a/requirements.yml +++ /dev/null @@ -1,19 +0,0 @@ -- name: composer - src: geerlingguy.composer - version: 1.6.1 - -- name: ntp - src: geerlingguy.ntp - version: 1.5.2 - -- name: logrotate - src: nickhammond.logrotate - version: e7a498d - -- name: swapfile - src: kamaln7.swapfile - version: 4850d8a - -- name: mailhog - src: geerlingguy.mailhog - version: 2.1.3 diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index 2a6d4a1f4f..6651ac5ec3 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -1,21 +1,20 @@ ntp_timezone: Etc/UTC apt_packages_default: - - python-software-properties - - python-pycurl - - build-essential - - python-mysqldb - - curl - - git-core - - dbus - - libnss-myhostname + build-essential: "{{ apt_package_state }}" + cron: "{{ apt_package_state }}" + curl: "{{ apt_package_state }}" + dbus: "{{ apt_package_state }}" + ghostscript: "{{ apt_package_state }}" + git: "{{ apt_package_state }}" + imagemagick: "{{ apt_package_state }}" + libgs-dev: "{{ apt_package_state }}" + libnss-myhostname: "{{ apt_package_state }}" + python3: "{{ apt_package_state }}" + python3-software-properties: "{{ apt_package_state }}" + python3-pymysql: "{{ apt_package_state }}" + python3-pycurl: "{{ apt_package_state }}" + unzip: "{{ apt_package_state }}" -apt_packages_custom: [] -apt_packages: "{{ apt_packages_default + apt_packages_custom }}" - -openssh_6_8_plus: "{{ (lookup('pipe', 'ssh -V 2>&1')) | regex_replace('(.*OpenSSH_([\\d\\.]*).*)', '\\2') | version_compare('6.8', '>=') }}" -overlapping_ciphers: "[{% for cipher in (sshd_ciphers_default + sshd_ciphers_extra) if cipher in ssh_client_ciphers %}'{{ cipher }}',{% endfor %}]" -overlapping_kex: "[{% for kex in (sshd_kex_algorithms_default + sshd_kex_algorithms_extra) if kex in ssh_client_kex %}'{{ kex }}',{% endfor %}]" -overlapping_macs: "[{% for mac in (sshd_macs_default + sshd_macs_extra) if mac in ssh_client_macs %}'{{ mac }}',{% endfor %}]" -host_key_types: "[{% for path in sshd_host_keys %}'{{ path | regex_replace('/etc/ssh/ssh_host_(.+)_key', '\\1') | regex_replace('dsa', 'ssh-dss')}}',{% endfor %}]" -overlapping_host_keys: "{% for key in host_key_types if key in ssh_client_host_key_algorithms %}{{ key }},{% endfor %}" +apt_packages_custom: {} +apt_packages: "{{ apt_packages_default | combine(apt_packages_custom) }}" diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index cab7d53088..96c4a52c09 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -6,10 +6,18 @@ service: name: memcached state: restarted + become: yes - name: reload php-fpm service: - name: php7.1-fpm + name: php{{ php_version }}-fpm state: reloaded + become: yes + +- name: restart journald + service: + name: systemd-journald + state: restarted + become: yes - import_tasks: reload_nginx.yml diff --git a/roles/common/tasks/clean-apt-sources.yml b/roles/common/tasks/clean-apt-sources.yml new file mode 100644 index 0000000000..b4346539f2 --- /dev/null +++ b/roles/common/tasks/clean-apt-sources.yml @@ -0,0 +1,19 @@ +--- +- name: Clean stale APT sources from /etc/apt/sources.list.d + find: + paths: /etc/apt/sources.list.d + patterns: + - '*.list' + - '*.list.save' + - '*.list.distUpgrade' + use_regex: false + register: stale_sources + become: true + +- name: Remove stale APT source files + file: + path: "{{ item.path }}" + state: absent + loop: "{{ stale_sources.files }}" + when: stale_sources.matched > 0 + become: true diff --git a/roles/common/tasks/disable_challenge_sites.yml b/roles/common/tasks/disable_challenge_sites.yml index 455d7f0cad..e7999fe65b 100644 --- a/roles/common/tasks/disable_challenge_sites.yml +++ b/roles/common/tasks/disable_challenge_sites.yml @@ -3,5 +3,6 @@ file: path: "{{ nginx_path }}/sites-enabled/letsencrypt-{{ item }}.conf" state: absent - with_items: "{{ wordpress_sites.keys() }}" + loop: "{{ wordpress_sites.keys() | list }}" notify: reload nginx + become: yes diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index f6a59531b8..2c1a3e25ad 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -2,81 +2,76 @@ - name: Validate wordpress_sites fail: msg: "{{ lookup('template', 'wordpress_sites.j2') }}" - when: wordpress_sites.keys() | difference(vault_wordpress_sites.keys()) | count + when: wordpress_sites.keys() | difference(vault_wordpress_sites.keys()) | count > 0 tags: [wordpress] - name: Validate format of site_hosts fail: msg: "{{ lookup('template', 'site_hosts.j2') }}" - with_dict: "{{ wordpress_sites }}" - when: item.value.site_hosts | rejectattr('canonical', 'defined') | list | count + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: item.value.site_hosts | rejectattr('canonical', 'defined') | list | count > 0 tags: [letsencrypt, wordpress] -- name: Validate Ubuntu version - debug: - msg: | - Trellis is built for Ubuntu 16.04 Xenial as of https://github.com/roots/trellis/pull/626 +- name: Import PHP version specific vars + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - '{{ php_version }}.yml' # e.g. 8.2.yml + - version-specific-defaults.yml + paths: + - "{{ playbook_dir }}/roles/php/vars/" - Your Ubuntu version is {{ ansible_distribution_version }} {{ ansible_distribution_release }} + tags: [memcached, php, sshd] - We recommend you re-create your server to get the best experience. - - Note: both of these methods will delete all your existing data. It's up to you to backup what's needed and restore it. - - Development via Vagrant: `vagrant destroy && vagrant up` - - Staging/Production: Create a new server with Ubuntu 16.04 and provision - when: ansible_distribution_release == 'trusty' - run_once: true - -- name: Check whether passlib is needed +- name: Verify dict format for package component variables fail: - msg: | - Ansible on OS X requires python passlib module to create user password hashes - - sudo easy_install pip - pip install passlib - when: env != 'development' and darwin_without_passlib | default(false) - run_once: true - -- name: Retrieve local SSH client's settings per host - set_fact: - ssh_client_ciphers: "{{ lookup('pipe', 'ssh -ttG ' + ansible_host + ' | grep ciphers') }}" - ssh_client_kex: "{{ lookup('pipe', 'ssh -ttG ' + ansible_host + ' | grep kexalgorithms') }}" - ssh_client_macs: "{{ lookup('pipe', 'ssh -ttG ' + ansible_host + ' | grep macs') }}" - ssh_client_host_key_algorithms: "{{ lookup('pipe', 'ssh -ttG ' + ansible_host + ' | grep hostkeyalgorithms') }}" - when: openssh_6_8_plus and validate_ssh | default(true) - tags: [sshd] - -- name: Validate compatible settings between SSH client and server - assert: - that: - - overlapping_ciphers | count - - overlapping_kex | count - - overlapping_macs | count - - overlapping_host_keys | count - msg: "{{ lookup('template', 'validate_ssh_msg.j2') }}" - when: openssh_6_8_plus and validate_ssh | default(true) - tags: [sshd] + msg: "{{ lookup('template', 'package_vars_wrong_format_msg.j2') }}" + when: package_vars_wrong_format | count > 0 + vars: + package_vars: + apt_packages_default: "{{ apt_packages_default }}" + apt_packages_custom: "{{ apt_packages_custom }}" + memcached_packages_default: "{{ memcached_packages_default }}" + memcached_packages_custom: "{{ memcached_packages_custom }}" + php_extensions_default: "{{ php_extensions_default }}" + php_extensions_custom: "{{ php_extensions_custom }}" + sshd_packages_default: "{{ sshd_packages_default }}" + sshd_packages_custom: "{{ sshd_packages_custom }}" + package_vars_wrong_format: "{{ package_vars | dict2items | rejectattr('value', 'mapping') | map(attribute='key') | list }}" + tags: [memcached, php, sshd] + +- name: Verify dict format for package combined variables + fail: + msg: "{{ lookup('template', 'package_vars_wrong_format_msg.j2') }}" + when: package_vars_wrong_format | count > 0 + vars: + package_vars: + apt_packages: "{{ apt_packages }}" + memcached_packages: "{{ memcached_packages }}" + php_extensions: "{{ php_extensions }}" + sshd_packages: "{{ sshd_packages }}" + package_vars_wrong_format: "{{ package_vars | dict2items | rejectattr('value', 'mapping') | map(attribute='key') | list }}" + tags: [memcached, php, sshd] + +- name: Clean old APT sources + import_tasks: clean-apt-sources.yml + when: apt_clean_sources | default(false) + +- name: Update apt packages + apt: + update_cache: yes - name: Checking essentials apt: - name: "{{ item }}" - state: present - update_cache: true + name: "{{ item.key }}" + state: "{{ item.value }}" cache_valid_time: "{{ apt_cache_valid_time }}" - with_items: "{{ apt_packages }}" - -- name: Validate timezone variable - stat: - path: /usr/share/zoneinfo/{{ ntp_timezone }} - register: timezone_path - changed_when: false - -- name: Explain timezone error - fail: - msg: "{{ ntp_timezone }} is not a valid timezone. For a list of valid timezones, check https://php.net/manual/en/timezones.php" - when: not timezone_path.stat.exists + loop: "{{ apt_packages | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Add myhostname to nsswitch.conf to ensure resolvable hostname lineinfile: @@ -87,15 +82,24 @@ regexp: ^(hosts\:((?!myhostname).)*)$ state: present -- name: Generate SSH key for vagrant user - user: - name: vagrant - generate_ssh_key: yes - when: env == 'development' - -- name: Retrieve SSH client IP - ipify_facts: - connection: local - become: no - when: env != 'development' and ssh_client_ip_lookup | default(true) - tags: [fail2ban, ferm] +- block: + - name: Retrieve SSH client IP + ipify_facts: + delegate_to: localhost + become: no + when: env != 'development' and ssh_client_ip_lookup | default(true) + tags: [fail2ban, ferm] + rescue: + - name: Fail when unable to retrieve SSH client IP + fail: + msg: "External IP resolution failed. Check that your DNS servers are working. Try to disable DNSCrypt if you are using it." + +- name: Restrict journal log size + lineinfile: + backup: yes + dest: /etc/systemd/journald.conf + insertafter: "^[Journal]" + line: "SystemMaxUse={{ max_journal_size }}" + regexp: "^#?(SystemMaxUse=.*?)$" + state: present + notify: restart journald diff --git a/roles/common/tasks/reload_nginx.yml b/roles/common/tasks/reload_nginx.yml index 952a6082e0..f11305fb0e 100644 --- a/roles/common/tasks/reload_nginx.yml +++ b/roles/common/tasks/reload_nginx.yml @@ -2,8 +2,11 @@ - name: reload nginx command: nginx -t notify: "{{ (role_path | basename == 'common') | ternary('perform nginx reload', omit) }}" + changed_when: true + become: yes - name: perform nginx reload service: name: nginx state: reloaded + become: yes diff --git a/roles/common/templates/package_vars_wrong_format_msg.j2 b/roles/common/templates/package_vars_wrong_format_msg.j2 new file mode 100644 index 0000000000..196368b904 --- /dev/null +++ b/roles/common/templates/package_vars_wrong_format_msg.j2 @@ -0,0 +1,4 @@ +The following variables must be formatted as dicts: + {{ package_vars_wrong_format | to_nice_yaml | indent(2) }} + +See: https://github.com/roots/trellis/pull/881 diff --git a/roles/common/templates/validate_ssh_msg.j2 b/roles/common/templates/validate_ssh_msg.j2 deleted file mode 100644 index 6d49d95aef..0000000000 --- a/roles/common/templates/validate_ssh_msg.j2 +++ /dev/null @@ -1,32 +0,0 @@ -{% macro msg(param_name, ssh_client_values, sshd_config_values, overlap_values, param_var_name) %} -{% if not overlap_values | count %} -{{ param_name }} your SSH Client is making available for {{ ansible_host }}: -{% for item in ssh_client_values.replace(' ',',').split(',') if item != param_name | lower %} - - {{ item }} -{% endfor %} - -{{ (param_name != 'HostKeyAlgorithms') | ternary(param_name, 'HostKeys') }} the host {{ ansible_host }} will accept/use after sshd role: -{% for item in sshd_config_values %} - - {{ item }} -{% endfor %} - -Create a corresponding value between the two. Adjust either of the following: - - your SSH client's {{ param_name }} option (recommended) - - the `{{ param_var_name }}` Trellis variable - ---------------------------------------------------- - -{% endif %} -{% endmacro -%} - -Your local SSH client settings will not support the settings that the sshd role will apply to the SSH server (on {{ ansible_host }}). - -See https://github.com/roots/trellis/tree/master/roles/sshd#ciphers-kexalgorithms-and-macs ---------------------------------------------------- - -{{ msg('Ciphers', ssh_client_ciphers, sshd_ciphers_default + sshd_ciphers_extra, overlapping_ciphers, 'sshd_ciphers_extra') -}} -{{ msg('KexAlgorithms', ssh_client_kex, sshd_kex_algorithms_default + sshd_kex_algorithms_extra, overlapping_kex, 'sshd_kex_algorithms_extra') -}} -{{ msg('MACs', ssh_client_macs, sshd_macs_default + sshd_macs_extra, overlapping_macs, 'sshd_macs_extra') -}} -{{ msg('HostKeyAlgorithms', ssh_client_host_key_algorithms, sshd_host_keys, overlapping_host_keys, 'sshd_host_keys') -}} - -To disable this validation and warning, define `validate_ssh: false` diff --git a/roles/connection/defaults/main.yml b/roles/connection/defaults/main.yml deleted file mode 100644 index f6c42a8444..0000000000 --- a/roles/connection/defaults/main.yml +++ /dev/null @@ -1,5 +0,0 @@ -ansible_host_known: "{{ lookup('pipe', 'ssh-keygen -F ' + ansible_host + ' > /dev/null 2>&1 && echo True || echo False') }}" -ssh_config_host: "{{ lookup('pipe', 'ssh -G ' + ansible_host + ' 2>/dev/null | grep \"^hostname\" ||:') | regex_replace('^hostname ([^\\s]+)', '\\1') }}" -ssh_config_host_known: "{{ lookup('pipe', 'ssh-keygen -F ' + ssh_config_host + ' > /dev/null 2>&1 && echo True || echo False') }}" -openssh_6_5_plus: "{{ (lookup('pipe', 'ssh -V 2>&1')) | regex_replace('(.*OpenSSH_([\\d\\.]*).*)', '\\2') | version_compare('6.5', '>=') }}" -host_key_algorithms: "{{ openssh_6_5_plus | ternary('ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,ssh-rsa', 'ssh-rsa-cert-v01@openssh.com,ssh-rsa') }}" diff --git a/roles/connection/tasks/main.yml b/roles/connection/tasks/main.yml index 92ba31315d..73c2f018d6 100644 --- a/roles/connection/tasks/main.yml +++ b/roles/connection/tasks/main.yml @@ -6,21 +6,11 @@ ansible-playbook server.yml -e env={{ env | default('production') }} -u root --ask-pass when: dynamic_user | default(true) and ansible_user is not defined and cli_ask_pass | default(false) -- name: Specify preferred HostKeyAlgorithms for unknown hosts - set_fact: - ansible_ssh_extra_args: -o HostKeyAlgorithms={{ host_key_algorithms }} - register: preferred_host_key_algorithms - when: - - dynamic_host_key_algorithms | default(true) - - ansible_ssh_extra_args | default('') == '' - - not (ansible_host_known or ssh_config_host_known) - - name: Check whether Ansible can connect as {{ dynamic_user | default(true) | ternary('root', web_user) }} - local_action: | - command ansible {{ inventory_hostname }} -m raw -a whoami + command: | + ansible {{ inventory_hostname }} -m raw -a whoami -u {{ dynamic_user | default(true) | ternary('root', web_user) }} {{ cli_options | default('') }} -vvvv - environment: - ANSIBLE_SSH_ARGS: "{{ ssh_args_default }} {{ ansible_ssh_extra_args | default('') }}" + delegate_to: localhost failed_when: false changed_when: false check_mode: no @@ -53,26 +43,13 @@ - block: - name: Set remote user for each host set_fact: - ansible_user: "{{ ansible_user | default((connection_status.stdout_lines | intersect(['root', '\e[0;32mroot']) | count) | ternary('root', admin_user)) }}" + ansible_user: "{{ ansible_user | default(('| CHANGED |' in connection_status.stdout) | ternary('root', admin_user)) }}" check_mode: no - name: Announce which user was selected debug: msg: | Note: Ansible will attempt connections as user = {{ ansible_user }} - {% if not preferred_host_key_algorithms | skipped %} - - Note: The host `{{ ansible_host }}` was not detected in known_hosts - so Trellis prompted the host to offer a key type that will work with - the stronger key types Trellis configures on the server. This avoids future - connection failures due to changed host keys. Trellis used this SSH option: - - {{ ansible_ssh_extra_args }} - - To prevent Trellis from ever using this SSH option, add this to group_vars: - - dynamic_host_key_algorithms: false - {% endif %} - name: Load become password set_fact: diff --git a/roles/deploy/defaults/main.yml b/roles/deploy/defaults/main.yml index 8e04496fff..a0682cd86a 100644 --- a/roles/deploy/defaults/main.yml +++ b/roles/deploy/defaults/main.yml @@ -2,7 +2,7 @@ # - you must set a repository (no default) project_git_repo: "{{ project.repo }}" # - you can set the git ref to deploy (can be a branch, tag or commit hash) -project_version: "{{ project.branch | default('master') }}" +project_version: "{{ branch is defined | ternary(branch, project.branch) | default('master') }}" # The source_path is used to fetch the tags from git, or synchronise via rsync. This way # you do not have to download/sync the entire project on every deploy @@ -20,6 +20,7 @@ project_templates: - name: .env config src: roles/deploy/templates/env.j2 dest: .env + mode: '0600' # The shared_children is a list of all files/folders in your project that need to be linked to a path in `/shared`. # For example a sessions directory or an uploads folder. They are created if they don't exist, with the type @@ -28,10 +29,10 @@ project_templates: # project_shared_children: # - path: app/sessions # src: sessions -# mode: '0755' // <- optional, must be quoted, defaults to `'0755'` if `directory` or `'0644'` if `file` -# type: directory // <- optional, defaults to `directory`, options: `directory` or `file` +# mode: '0755' // <- optional, use an octal number starting with 0 or quote it, defaults to `'0755'` if `directory` or `'0644'` if `file` +# type: directory // <- optional, defaults to `directory`, options: `directory` or `file` project_shared_children: - - path: web/app/uploads + - path: "{{ project_public_path }}/{{ project_upload_path }}" src: uploads # The project_environment is a list of environment variables that can be used in hooks @@ -48,19 +49,32 @@ project_current_path: "{{ project.current_path | default('current') }}" # Whether to run `wp core update-db` at end of each deploy update_db_on_deploy: true +# Whether to flush rewrite rules at end of each deploy +flush_rewrite_rules_on_deploy: true + +# Most scripts are used in development instead of remote servers. Use with caution. +composer_no_scripts: true +# Whether to run `composer check-platform-reqs`. +composer_platform_requirements_check: true +# Whether to autoload classes from classmap only. +composer_classmap_authoritative: true + # Helpers project: "{{ wordpress_sites[site] }}" project_root: "{{ www_root }}/{{ site }}" -project_local_path: "{{ (lookup('env', 'USER') == 'vagrant') | ternary(project_root + '/' + project_current_path, project.local_path) }}" +project_local_path: "{{ (lookup('env', 'USER') == 'web') | ternary(project_root + '/' + project_current_path, project.local_path) }}" +project_public_path: "{{ project.public_path | default('web') }}" +project_upload_path: "{{ project.upload_path | default('app/uploads') }}" # Deploy hooks -# For list of hooks and explanation, see https://roots.io/trellis/docs/deploys/#hooks +# For list of hooks and explanation, see https://roots.io/trellis/docs/deployments/#hooks deploy_build_before: - "{{ playbook_dir }}/deploy-hooks/build-before.yml" deploy_build_after: - "{{ playbook_dir }}/roles/deploy/hooks/build-after.yml" + - "{{ playbook_dir }}/deploy-hooks/build-after.yml" # - "{{ playbook_dir }}/deploy-hooks/sites/{{ site }}-build-after.yml" deploy_finalize_before: diff --git a/roles/deploy/files/tmp_multisite_constants.php b/roles/deploy/files/tmp_multisite_constants.php deleted file mode 100644 index e468cc8bfc..0000000000 --- a/roles/deploy/files/tmp_multisite_constants.php +++ /dev/null @@ -1,7 +0,0 @@ -<?php -error_reporting(E_ALL & ~E_NOTICE); -define('MULTISITE', false); -define('SUBDOMAIN_INSTALL', false); -define('WPMU_PLUGIN_DIR', null); -define('WP_PLUGIN_DIR', null); -define('WP_USE_THEMES', false); diff --git a/roles/deploy/hooks/build-after.yml b/roles/deploy/hooks/build-after.yml index 6c8c462984..4822c7549c 100644 --- a/roles/deploy/hooks/build-after.yml +++ b/roles/deploy/hooks/build-after.yml @@ -9,15 +9,57 @@ msg: "Unable to find a `composer.json` file in the root of '{{ deploy_helper.new_release_path }}'. Make sure your repo has a `composer.json` file in its root or edit `repo_subtree_path` for '{{ site }}' in `wordpress_sites.yml` so it points to the directory with a `composer.json` file." when: not composer_json.stat.exists -- name: Setup packagist.com authentication +- name: Setup composer authentications (HTTP Basic) composer: command: config - arguments: --auth http-basic.repo.packagist.com token {{ project.packagist_token }} + arguments: --auth http-basic.{{ item.hostname | quote }} {{ item.username | quote }} {{ item.password | default("") | quote }} working_dir: "{{ deploy_helper.new_release_path }}" no_log: true - when: project.packagist_token is defined + changed_when: false + when: + - item.hostname is defined and item.hostname != "" + - item.username is defined and item.username != "" + loop: "{{ composer_authentications_using_basic_auth }}" + loop_control: + label: "{{ item.type | default('default-type') }}.{{ item.hostname }}" + +- name: Setup composer authentications (BitBucket OAuth) + composer: + command: config + arguments: --auth bitbucket-oauth.{{ item.hostname | quote }} {{ item.consumer_key | quote }} {{ item.consumer_secret | quote }} + working_dir: "{{ deploy_helper.new_release_path }}" + no_log: true + changed_when: false + when: + - item.hostname is defined and item.hostname != "" + - item.consumer_key is defined and item.consumer_key != "" + - item.consumer_secret is defined and item.consumer_secret != "" + loop: "{{ composer_authentications_using_bitbucket_oauth }}" + loop_control: + label: "{{ item.type }}.{{ item.hostname }}" + +- name: Setup composer authentications (Other Tokens) + composer: + command: config + arguments: --auth {{ item.type | quote }}.{{ item.hostname | quote }} {{ item.token | quote }} + working_dir: "{{ deploy_helper.new_release_path }}" + no_log: true + changed_when: false + when: + - item.hostname is defined and item.hostname != "" + - item.token is defined and item.token != "" + loop: "{{ composer_authentications_using_other_token }}" + loop_control: + label: "{{ item.type }}.{{ item.hostname }}" + +- name: Run composer check + composer: + command: check-platform-reqs + working_dir: "{{ deploy_helper.new_release_path }}" + when: composer_platform_requirements_check - name: Install Composer dependencies composer: - no_scripts: yes + no_scripts: "{{ composer_no_scripts }}" + classmap_authoritative: "{{ composer_classmap_authoritative }}" working_dir: "{{ deploy_helper.new_release_path }}" diff --git a/roles/deploy/hooks/finalize-after.yml b/roles/deploy/hooks/finalize-after.yml index d60809d0da..7e0152d5fe 100644 --- a/roles/deploy/hooks/finalize-after.yml +++ b/roles/deploy/hooks/finalize-after.yml @@ -6,21 +6,21 @@ chdir: "{{ deploy_helper.current_path }}" register: site_transient_theme_roots changed_when: site_transient_theme_roots.stdout != '' - when: update_wp_theme_paths | default(true) | bool + when: project.update_wp_theme_paths | default(update_wp_theme_paths | default(true)) | bool - name: Update WP theme paths command: > - wp option set {{ item[0].option }} + wp option set {{ item[0].item }} {{ item[1] | regex_replace('.*' + deploy_helper.releases_path + '/[^/]*(.*)', deploy_helper.new_release_path + '\1') }} {% if project.multisite.enabled | default(false) %} --url={{ item[1].split(' ')[0] }}{% endif %} args: chdir: "{{ deploy_helper.current_path }}" - when: update_wp_theme_paths | default(true) | bool - with_subelements: - - "[{% for result in wp_template_root.results %}{'option': '{{ result.item }}', 'stdout_lines': {{ result.stdout_lines | default ([]) | select('search', deploy_helper.releases_path) | list }}},{% endfor %}]" - - stdout_lines + loop: "{{ wp_template_root.results | subelements('stdout_lines', skip_missing=true) }}" + loop_control: + label: "{{ item[0].item }}" + when: project.update_wp_theme_paths | default(update_wp_theme_paths | default(true)) | bool and item[1] is match(deploy_helper.releases_path) - - name: Warn about updating network database. + - name: Warn about updating network database debug: msg: "Updating the network database could take a long time with a large number of sites." when: project.update_db_on_deploy | default(update_db_on_deploy) and project.multisite.enabled | default(false) @@ -31,9 +31,18 @@ chdir: "{{ deploy_helper.current_path }}" when: project.update_db_on_deploy | default(update_db_on_deploy) + - name: Flush rewrite rules + command: wp rewrite flush + args: + chdir: "{{ deploy_helper.current_path }}" + when: project.flush_rewrite_rules_on_deploy | default(flush_rewrite_rules_on_deploy) + + - name: Flush cache + command: wp cache flush + args: + chdir: "{{ deploy_helper.current_path }}" + when: wp_installed.rc == 0 - name: Reload php-fpm - shell: sudo service php7.1-fpm reload - args: - warn: false + shell: sudo service php{{ php_version }}-fpm reload diff --git a/roles/deploy/hooks/finalize-before.yml b/roles/deploy/hooks/finalize-before.yml index ec40bc9ee2..b219df86b0 100644 --- a/roles/deploy/hooks/finalize-before.yml +++ b/roles/deploy/hooks/finalize-before.yml @@ -1,16 +1,45 @@ --- -- name: Create file with multisite constants defined as false - copy: - src: "tmp_multisite_constants.php" - dest: "{{ deploy_helper.shared_path }}/tmp_multisite_constants.php" +- name: Clean up unused, temporary PHP file with multisite constants that had been used for WordPress Installed checks. + file: + state: absent + path: "{{ deploy_helper.shared_path }}/tmp_multisite_constants.php" + +- name: WordPress Installed (non-multisite)? + block: + - name: "Invoke 'wp core is-installed' command" + command: wp core is-installed --skip-plugins --skip-themes + args: + chdir: "{{ deploy_helper.new_release_path }}" + register: wp_installed_singlesite + changed_when: false + failed_when: wp_installed_singlesite.stderr | length > 0 or wp_installed_singlesite.rc > 1 + + - name: Set "WordPress installed?" result variable (from non-multisite) + set_fact: + wp_installed: "{{ wp_installed_singlesite }}" + when: + - not project.multisite.enabled | default(false) + +- name: WordPress Installed (multisite)? + block: + - name: Set variables used in "WordPress Installed (multisite)?" check + set_fact: + multisite_non_setup_db_error: "WordPress database error Table '{{ site_env.db_name }}.wp_blogs' doesn't exist" + + - name: "Invoke 'wp core is-installed' command" + command: wp core is-installed --skip-plugins --skip-themes + args: + chdir: "{{ deploy_helper.new_release_path }}" + register: wp_installed_multisite + changed_when: false + failed_when: (wp_installed_multisite.stderr | length > 0 and wp_installed_multisite.stderr is not match(multisite_non_setup_db_error)) or wp_installed_multisite.rc > 1 + + - name: Set "WordPress installed?" result variable (from multisite) + set_fact: + wp_installed: "{{ wp_installed_multisite }}" + when: + - project.multisite.enabled | default(false) -- name: WordPress Installed? - command: wp core is-installed --skip-plugins --skip-themes --require={{ deploy_helper.shared_path }}/tmp_multisite_constants.php - args: - chdir: "{{ deploy_helper.new_release_path }}" - register: wp_installed - changed_when: false - failed_when: wp_installed.stderr != "" - name: Get WP theme template and stylesheet roots shell: > @@ -23,10 +52,10 @@ chdir: "{{ deploy_helper.current_path }}" register: wp_template_root changed_when: false - failed_when: wp_template_root.stderr | default('') != '' + failed_when: wp_template_root.stderr | default('') is not match("(|.*Could not get '" + item + "' option\. Does it exist\?)") when: - wp_installed.rc == 0 - - update_wp_theme_paths | default(true) | bool - with_items: + - project.update_wp_theme_paths | default(update_wp_theme_paths | default(true)) | bool + loop: - template_root - stylesheet_root diff --git a/roles/deploy/tasks/build.yml b/roles/deploy/tasks/build.yml index c7cf9be7d2..499d65eed3 100644 --- a/roles/deploy/tasks/build.yml +++ b/roles/deploy/tasks/build.yml @@ -1,8 +1,17 @@ --- -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_build_before | default([]) }}" +- name: Check if deploy_build_before scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_build_before_paths + loop: "{{ deploy_build_before | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_build_before_paths.results }}" loop_control: loop_var: include_path + label: "{{ include_path.item }}" + when: include_path.stat.exists tags: deploy-build-before - name: Copy project templates @@ -10,21 +19,38 @@ src: "{{ item.src }}" dest: "{{ deploy_helper.new_release_path }}/{{ item.dest }}" mode: "{{ item.mode | default('0644') }}" - with_items: "{{ project_templates }}" + loop: "{{ project.project_templates | default(project_templates) }}" + loop_control: + label: "{{ item.name }}" - name: Check if project folders exist stat: path: "{{ deploy_helper.current_path }}/{{ item }}" register: project_folder_paths - with_items: "{{ project_copy_folders }}" + loop: "{{ project.project_copy_folders | default(project_copy_folders) }}" - name: Copy project folders - command: cp -rp {{ deploy_helper.current_path }}/{{ item.item }} {{ deploy_helper.new_release_path }} - with_items: "{{ project_folder_paths.results }}" + copy: + src: "{{ deploy_helper.current_path }}/{{ item.item }}/" + dest: "{{ deploy_helper.new_release_path }}/{{ item.item }}" + remote_src: true + mode: 'preserve' + loop: "{{ project_folder_paths.results }}" + loop_control: + label: "{{ item.item }}" when: item.stat.exists -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_build_after | default([]) }}" +- name: Check if deploy_build_after scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_build_after_paths + loop: "{{ deploy_build_after | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_build_after_paths.results }}" loop_control: loop_var: include_path + label: "{{ include_path.item }}" + when: include_path.stat.exists tags: deploy-build-after diff --git a/roles/deploy/tasks/finalize.yml b/roles/deploy/tasks/finalize.yml index 996873919d..843f46632c 100644 --- a/roles/deploy/tasks/finalize.yml +++ b/roles/deploy/tasks/finalize.yml @@ -1,8 +1,17 @@ --- -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_finalize_before | default([]) }}" +- name: Check if deploy_finalize_before scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_finalize_before_paths + loop: "{{ deploy_finalize_before | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_finalize_before_paths.results }}" loop_control: loop_var: include_path + label: "{{ include_path.item }}" + when: include_path.stat.exists tags: deploy-finalize-before - name: Finalize the deploy @@ -11,11 +20,21 @@ path: "{{ project_root }}" release: "{{ deploy_helper.new_release }}" state: finalize + keep_releases: "{{ project.deploy_keep_releases | default(deploy_keep_releases | default(omit)) }}" + +- name: Check if deploy_finalize_after scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_finalize_after_paths + loop: "{{ deploy_finalize_after | default([]) }}" -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_finalize_after | default([]) }}" +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_finalize_after_paths.results }}" loop_control: loop_var: include_path + label: "{{ include_path.item }}" + when: include_path.stat.exists tags: deploy-finalize-after - debug: diff --git a/roles/deploy/tasks/initialize.yml b/roles/deploy/tasks/initialize.yml index a78fefab90..df75215f5e 100644 --- a/roles/deploy/tasks/initialize.yml +++ b/roles/deploy/tasks/initialize.yml @@ -1,8 +1,16 @@ --- -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_initialize_before | default([]) }}" +- name: Check if deploy_initialize_before scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_initialize_before_paths + loop: "{{ deploy_initialize_before | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_initialize_before_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-initialize-before - name: Initialize @@ -11,8 +19,16 @@ path: "{{ project_root }}" state: present -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_initialize_after | default([]) }}" +- name: Check if deploy_initialize_after scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_initialize_after_paths + loop: "{{ deploy_initialize_after | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_initialize_after_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-initialize-after diff --git a/roles/deploy/tasks/main.yml b/roles/deploy/tasks/main.yml index 2e6b5c27f7..b8c102d4ac 100644 --- a/roles/deploy/tasks/main.yml +++ b/roles/deploy/tasks/main.yml @@ -1,8 +1,16 @@ --- -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_before | default([]) }}" +- name: Check if deploy_before scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_before_paths + loop: "{{ deploy_before | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_before_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-before - import_tasks: initialize.yml @@ -12,8 +20,16 @@ - import_tasks: share.yml - import_tasks: finalize.yml -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_after | default([]) }}" +- name: Check if deploy_after scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_after_paths + loop: "{{ deploy_after | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_after_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-after diff --git a/roles/deploy/tasks/prepare.yml b/roles/deploy/tasks/prepare.yml index 9181b43f51..33ae769999 100644 --- a/roles/deploy/tasks/prepare.yml +++ b/roles/deploy/tasks/prepare.yml @@ -1,15 +1,18 @@ --- -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_prepare_before | default([]) }}" +- name: Check if deploy_prepare_before scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_prepare_before_paths + loop: "{{ deploy_prepare_before | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_prepare_before_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-prepare-before -- name: write unfinished file - file: - path: "{{ project_source_path }}/{{ deploy_helper.unfinished_filename }}" - state: touch - - name: Check for project repo subtree stat: path: "{{ project_source_path }}/{{ project.repo_subtree_path }}" @@ -24,22 +27,43 @@ - name: Create new release dir file: path: "{{ deploy_helper.new_release_path }}" + mode: '0755' state: directory - name: Run git archive to populate new build dir - shell: git archive {{ project_version }} | tar xf - -C {{ deploy_helper.new_release_path }} + shell: | + set -eo pipefail + git archive {{ project_version }} | tar xf - -C {{ deploy_helper.new_release_path }} args: chdir: "{{ project_source_path }}" + executable: /bin/bash when: project.repo_subtree_path is not defined - name: Run git archive with subdirectory to populate new build dir - shell: git archive {{ project_version }} {{ project.repo_subtree_path }} | tar -x --strip-components {{ project.repo_subtree_path.split('/') | count }} -f - -C {{ deploy_helper.new_release_path }} + shell: | + set -eo pipefail + git archive {{ project_version }} {{ project.repo_subtree_path }} | tar -x --strip-components {{ project.repo_subtree_path.split('/') | count }} -f - -C {{ deploy_helper.new_release_path }} args: chdir: "{{ project_source_path }}" + executable: /bin/bash when: project.repo_subtree_path is defined -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_prepare_after | default([]) }}" +- name: write unfinished file + file: + path: "{{ deploy_helper.new_release_path }}/{{ deploy_helper.unfinished_filename }}" + mode: '0744' + state: touch + +- name: Check if deploy_prepare_after scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_prepare_after_paths + loop: "{{ deploy_prepare_after | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_prepare_after_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-prepare-after diff --git a/roles/deploy/tasks/share.yml b/roles/deploy/tasks/share.yml index 0537a8887f..ee3166d549 100644 --- a/roles/deploy/tasks/share.yml +++ b/roles/deploy/tasks/share.yml @@ -1,8 +1,17 @@ --- -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_share_before | default([]) }}" +- name: Check if deploy_share_before scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_share_before_paths + loop: "{{ deploy_share_before | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_share_before_paths.results }}" loop_control: loop_var: include_path + label: "{{ include_path.item }}" + when: include_path.stat.exists tags: deploy-share-before - name: Ensure shared sources are present -- directories @@ -10,7 +19,9 @@ path: "{{ deploy_helper.shared_path }}/{{ item.src }}" state: directory mode: "{{ item.mode | default('0755') }}" - with_items: "{{ project_shared_children }}" + loop: "{{ project.project_shared_children | default(project_shared_children) }}" + loop_control: + label: "{{ item.path }}" when: item.type | default('directory') | lower == 'directory' - name: Ensure shared sources are present -- files' parent directories @@ -18,7 +29,9 @@ path: "{{ deploy_helper.shared_path }}/{{ item.src | dirname }}" state: directory mode: '0755' - with_items: "{{ project_shared_children }}" + loop: "{{ project.project_shared_children | default(project_shared_children) }}" + loop_control: + label: "{{ item.path }}" when: item.type | default('directory') | lower == 'file' - name: Ensure shared sources are present -- files @@ -26,30 +39,48 @@ path: "{{ deploy_helper.shared_path }}/{{ item.src }}" state: touch mode: "{{ item.mode | default('0644') }}" - with_items: "{{ project_shared_children }}" + loop: "{{ project.project_shared_children | default(project_shared_children) }}" + loop_control: + label: "{{ item.path }}" when: item.type | default('directory') | lower == 'file' - name: Ensure parent directories for shared paths are present file: path: "{{ deploy_helper.new_release_path }}/{{ item.path | dirname }}" + mode: '0755' state: directory - with_items: "{{ project_shared_children }}" + loop: "{{ project.project_shared_children | default(project_shared_children) }}" + loop_control: + label: "{{ item.path }}" - name: Ensure shared paths are absent file: path: "{{ deploy_helper.new_release_path }}/{{ item.path }}" state: absent - with_items: "{{ project_shared_children }}" + loop: "{{ project.project_shared_children | default(project_shared_children) }}" + loop_control: + label: "{{ item.path }}" - name: Create shared symlinks file: path: "{{ deploy_helper.new_release_path }}/{{ item.path }}" src: "{{ deploy_helper.shared_path }}/{{ item.src }}" state: link - with_items: "{{ project_shared_children }}" + loop: "{{ project.project_shared_children | default(project_shared_children) }}" + loop_control: + label: "{{ item.path }}" + +- name: Check if deploy_share_after scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_share_after_paths + loop: "{{ deploy_share_after | default([]) }}" -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_share_after | default([]) }}" +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_share_after_paths.results }}" loop_control: loop_var: include_path + label: "{{ include_path.item }}" + when: include_path.stat.exists tags: deploy-share-after diff --git a/roles/deploy/tasks/update.yml b/roles/deploy/tasks/update.yml index 1be9bc1ff8..0039e4a780 100644 --- a/roles/deploy/tasks/update.yml +++ b/roles/deploy/tasks/update.yml @@ -1,8 +1,16 @@ --- -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_update_before | default([]) }}" +- name: Check if deploy_update_before scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_update_before_paths + loop: "{{ deploy_update_before | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_update_before_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-update-before - name: Add known_hosts @@ -11,33 +19,18 @@ key: "{{ item.key | default(omit) }}" path: "{{ item.path | default(omit) }}" state: "{{ item.state | default('present') }}" - with_items: "{{ known_hosts | default([]) }}" - -- name: Check whether project source path is a git repo - stat: - path: "{{ project_source_path }}/.git" - register: git_project - -- name: Get current git remote URL - command: git config --get remote.origin.url - args: - chdir: "{{ project_source_path }}" - register: remote_origin_url - when: git_project.stat.exists - changed_when: false - -- name: Update git remote URL - command: git remote set-url origin {{ project_git_repo }} - args: - chdir: "{{ project_source_path }}" - when: git_project.stat.exists and remote_origin_url.stdout != project_git_repo + loop: "{{ known_hosts | default([]) }}" + loop_control: + label: "{{ item.name }}" - name: Clone project files git: repo: "{{ project_git_repo }}" dest: "{{ project_source_path }}" version: "{{ project_version }}" - accept_hostkey: "{{ repo_accept_hostkey | default(true) }}" + accept_hostkey: "{{ project.repo_accept_hostkey | default(repo_accept_hostkey | default(true)) }}" + force: yes + depth: 1 ignore_errors: true no_log: true register: git_clone @@ -45,14 +38,32 @@ - name: Failed connection to remote repo fail: msg: | - Git repo {{ project.repo }} cannot be accessed. Please verify the repository exists and you have SSH forwarding set up correctly. + Git repo {{ project.repo }} on branch {{ project_version }} cannot be accessed. Please verify the repository/branch are correct and you have SSH forwarding set up correctly. More info: > https://roots.io/trellis/docs/deploys/#ssh-keys > https://roots.io/trellis/docs/ssh-keys/#cloning-remote-repo-using-ssh-agent-forwarding - when: git_clone | failed -- include_tasks: "{{ include_path }}" - with_items: "{{ deploy_update_after | default([]) }}" + Error: + {{ git_clone.msg | default(git_clone.stderr) }} + when: git_clone is failed + +- name: Remove untracked files from project folder + command: git clean -fdx + args: + chdir: "{{ project_source_path }}" + register: git_clean + changed_when: not not(git_clean.stdout) + +- name: Check if deploy_update_after scripts exist + stat: + path: "{{ item }}" + delegate_to: localhost + register: deploy_update_after_paths + loop: "{{ deploy_update_after | default([]) }}" + +- include_tasks: "{{ include_path.item }}" + loop: "{{ deploy_update_after_paths.results }}" loop_control: loop_var: include_path + when: include_path.stat.exists tags: deploy-update-after diff --git a/roles/deploy/vars/main.yml b/roles/deploy/vars/main.yml index cb51f14823..2b5e6ba81e 100644 --- a/roles/deploy/vars/main.yml +++ b/roles/deploy/vars/main.yml @@ -7,5 +7,28 @@ wordpress_env_defaults: wp_home: "{{ project.ssl.enabled | default(false) | ternary('https', 'http') }}://{{ project.site_hosts | map(attribute='canonical') | first }}" wp_siteurl: "{{ project.ssl.enabled | default(false) | ternary('https', 'http') }}://{{ project.site_hosts | map(attribute='canonical') | first }}/wp" domain_current_site: "{{ project.site_hosts | map(attribute='canonical') | first }}" + git_sha: "{{ git_clone.after }}" + release_version: "{{ deploy_helper.new_release }}" + wp_debug_log: "{{ project_root }}/logs/debug.log" + wp_post_revisions: true -site_env: "{{ wordpress_env_defaults | combine(project.env | default({}), vault_wordpress_sites[site].env) }}" +# Redis cache environment variables for deployment +deploy_redis_cache_env: + wp_redis_host: "{{ project.object_cache.host | default('127.0.0.1') }}" + wp_redis_port: "{{ project.object_cache.port | default(6379) }}" + wp_redis_database: "{{ project.object_cache.database | default(0) }}" + wp_redis_prefix: "{{ project.object_cache.prefix | default(site | underscore + '_') }}" + wp_cache_key_salt: "{{ site }}_{{ env }}" + +# Memcached cache environment variables for deployment +deploy_memcached_cache_env: + wp_memcached_host: "{{ project.object_cache.host | default('127.0.0.1') }}" + wp_memcached_port: "{{ project.object_cache.port | default(11211) }}" + wp_memcached_prefix: "{{ project.object_cache.prefix | default(site | underscore + '_') }}" + wp_cache_key_salt: "{{ site }}_{{ env }}" + +site_env: "{{ wordpress_env_defaults | combine(vault_wordpress_env_defaults | default({}), deploy_object_cache_enabled_redis | ternary(deploy_redis_cache_env, {}), deploy_object_cache_enabled_memcached | ternary(deploy_memcached_cache_env, {}), project.env | default({}), vault_wordpress_sites[site].env) }}" + +# Helper variables for deploy object cache conditions +deploy_object_cache_enabled_redis: "{{ project.object_cache.enabled | default(false) and project.object_cache.provider | default('') == 'redis' }}" +deploy_object_cache_enabled_memcached: "{{ project.object_cache.enabled | default(false) and project.object_cache.provider | default('') == 'memcached' }}" diff --git a/roles/fail2ban/README.md b/roles/fail2ban/README.md index a3aa07b68e..16b707fc8a 100644 --- a/roles/fail2ban/README.md +++ b/roles/fail2ban/README.md @@ -1,19 +1,17 @@ -## What is ansible-fail2ban? +## What is this role? -It is an [ansible](http://www.ansible.com/home) role to install and configure fail2ban. +This role installs and configures [Fail2ban](https://github.com/fail2ban/fail2ban). -### What problem does it solve and why is it useful? - -Security is important and fail2ban is an excellent tool to harden your server with minimal or even no configuration. +Fail2ban is an excellent tool to harden your server with minimal configuration. ## Role variables -Below is a list of default values along with a description of what they do. +Below is a list of available variables, their description and their default value within Trellis. -``` +```yaml # Which log level should it be output as? -# Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. Default: ERROR -fail2ban_loglevel: WARNING +# Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. +fail2ban_loglevel: INFO # Where should log outputs be sent to? # SYSLOG, STDERR, STDOUT, file @@ -56,10 +54,25 @@ fail2ban_chain: INPUT # action_, action_mw, action_mwl fail2ban_action: action_ -# What services should fail2ban monitor? -# You can define fail2ban_services as an empty string to not monitor anything. +# Trellis by default only monitors SSH connections +# For available parameters, see fail2ban_services_custom below. +fail2ban_services_default: + - name: ssh + port: ssh + filter: sshd + logpath: /var/log/auth.log + +# In which folder did you place custom filters? +# Filters MUST have .conf.j2 extension to copied to the servers. +fail2ban_filter_templates_path: fail2ban_filters +``` + +The following list variable is available for custom services (to be set up in `group_vars`): + +```yaml +# Which additional services should fail2ban monitor? # You can define multiple services as a standard yaml list. -fail2ban_services: +fail2ban_services_custom: # The name of the service # REQUIRED. - name: ssh @@ -77,11 +90,11 @@ fail2ban_services: # OPTIONAL: Defaults to the protocol listed above. protocol: tcp - # What filter should it use? + # Which filter should it use? # REQUIRED. filter: sshd - # Which log path should it monitor? + # Which log file should it monitor? # REQUIRED. logpath: /var/log/auth.log @@ -96,24 +109,29 @@ fail2ban_services: # How should the ban be applied? # OPTIONAL: Defaults to the banaction listed above. banaction: iptables-multiport + ``` -## Example playbook +## Custom Settings -Let's say you want to edit a few values, you can do this by opening `group_vars/all` and then add the following: +To add services, you might add the following to `group_vars/all/security.yml`, e.g.: -``` -fail2ban_services: - - name: ssh - port: ssh - filter: sshd +```yaml +fail2ban_services_custom: + - name: wordpress + filter: wordpress logpath: /var/log/auth.log - - name: postfix - port: smtp,ssmtp - filter: postfix - logpath: /var/log/mail.log + maxretry: 2 ``` +To add the corresponding filter, add it to the folder specified in `fail2ban_filter_templates_path`, i.e. `fail2ban_filters` per default (next to the `group_vars` folder). The filter configuration must be of `.conf.j2` extension for Trellis to recognize it. + +Filters might be provided by plugins as `.conf` files: it is then enough to simply append the file name with `.j2`. It is not required to modify these provided filters, but you may customize them to your liking. + +To develop custom filters, refer to the Fail2ban wiki: [How Fail2ban works](https://github.com/fail2ban/fail2ban/wiki/How-fail2ban-works) and [How to ban something…](https://github.com/fail2ban/fail2ban/wiki/How-to-ban-something-other-as-host-(IP-address),-like-user-or-mail,-etc.) for simple filter rules or [Developing Filters](https://fail2ban.readthedocs.io/en/latest/filters.html) for complex setups. + +If you need to edit the default services, copy the `fail2ban_services_default` list from `roles/fail2ban/defaults/main.yml` to `group_vars/all/security.yml` and edit as needed. + ## Attribution Many thanks to [nickjj](https://github.com/nickjj/) for creating the [original version](https://github.com/nickjj/ansible-fail2ban/) of this role. diff --git a/roles/fail2ban/defaults/main.yml b/roles/fail2ban/defaults/main.yml index e75e2b194d..a57c78e9ab 100644 --- a/roles/fail2ban/defaults/main.yml +++ b/roles/fail2ban/defaults/main.yml @@ -1,9 +1,11 @@ --- +fail2ban_package: fail2ban + fail2ban_loglevel: INFO fail2ban_logtarget: /var/log/fail2ban.log fail2ban_socket: /var/run/fail2ban/fail2ban.sock -fail2ban_ignoreip: 127.0.0.1/8 {{ ip_whitelist | join(' ') }} +fail2ban_ignoreip: "{{ ip_whitelist | join(' ') }}" fail2ban_bantime: 600 fail2ban_maxretry: 6 @@ -17,8 +19,14 @@ fail2ban_chain: INPUT fail2ban_action: action_ -fail2ban_services: +fail2ban_services_default: - name: ssh port: ssh filter: sshd logpath: /var/log/auth.log + +fail2ban_services_custom: [] +fail2ban_services: "{{ fail2ban_services_default + fail2ban_services_custom }}" + +fail2ban_builtin_filter_templates_path: "{{ playbook_dir }}/roles/fail2ban/templates/filters" +fail2ban_filter_templates_path: fail2ban_filters diff --git a/roles/fail2ban/tasks/main.yml b/roles/fail2ban/tasks/main.yml index 3af663a73e..e11453c8f4 100644 --- a/roles/fail2ban/tasks/main.yml +++ b/roles/fail2ban/tasks/main.yml @@ -1,9 +1,8 @@ --- - name: ensure fail2ban is installed apt: - pkg: fail2ban - state: latest - update_cache: true + name: "{{ fail2ban_package }}" + state: "{{ fail2ban_package_state | default(apt_security_package_state) }}" cache_valid_time: "{{ apt_cache_valid_time }}" notify: - restart fail2ban @@ -12,12 +11,45 @@ template: src: "{{ item }}.j2" dest: /etc/fail2ban/{{ item }} - with_items: + mode: '0644' + loop: - jail.local - fail2ban.local notify: - restart fail2ban +- name: Check if fail2ban_filter_templates_path exists + stat: + path: "{{ fail2ban_filter_templates_path }}" + become: no + delegate_to: localhost + register: fail2ban_filter_templates_path_result + +- name: build list of fail2ban filter templates + find: + paths: "{{ fail2ban_filter_templates_path_result.stat.isdir is defined | ternary( + [fail2ban_builtin_filter_templates_path, fail2ban_filter_templates_path], + [fail2ban_builtin_filter_templates_path] + ) }}" + pattern: "*.conf.j2" + become: no + delegate_to: localhost + register: fail2ban_filter_templates + +- name: ensure configuration directory exists + file: + path: /etc/fail2ban/filter.d/ + state: directory + mode: '0755' + +- name: template fail2ban filters + template: + src: "{{ item }}" + dest: "/etc/fail2ban/filter.d/{{ item | basename | regex_replace('.j2$', '') }}" + mode: '0644' + loop: "{{ fail2ban_filter_templates.files | map(attribute='path') | list | sort(True) }}" + notify: restart fail2ban + - name: ensure fail2ban starts on a fresh reboot service: name: fail2ban diff --git a/roles/fail2ban/templates/filters/wordpress-wp-login.conf.j2 b/roles/fail2ban/templates/filters/wordpress-wp-login.conf.j2 new file mode 100644 index 0000000000..d0f9271098 --- /dev/null +++ b/roles/fail2ban/templates/filters/wordpress-wp-login.conf.j2 @@ -0,0 +1,2 @@ +[Definition] +failregex = ^<HOST> .* "POST .*wp-login\.php diff --git a/roles/fail2ban/templates/filters/wordpress-xmlrpc.conf.j2 b/roles/fail2ban/templates/filters/wordpress-xmlrpc.conf.j2 new file mode 100644 index 0000000000..6d8547146f --- /dev/null +++ b/roles/fail2ban/templates/filters/wordpress-xmlrpc.conf.j2 @@ -0,0 +1,2 @@ +[Definition] +failregex = ^<HOST> .* "POST .*xmlrpc\.php diff --git a/roles/ferm/defaults/main.yml b/roles/ferm/defaults/main.yml index edda2d9a98..17f623b5d0 100644 --- a/roles/ferm/defaults/main.yml +++ b/roles/ferm/defaults/main.yml @@ -1,4 +1,6 @@ --- +ferm_package: ferm + ferm_enabled: true ferm_limit_portscans: false diff --git a/roles/ferm/handlers/main.yml b/roles/ferm/handlers/main.yml index 16985eb8f5..c18fe1719c 100644 --- a/roles/ferm/handlers/main.yml +++ b/roles/ferm/handlers/main.yml @@ -1,4 +1,6 @@ --- - name: restart ferm - service: name=ferm state=restarted - when: ferm_enabled \ No newline at end of file + service: + name: ferm + state: restarted + when: ferm_enabled | bool diff --git a/roles/ferm/tasks/main.yml b/roles/ferm/tasks/main.yml index b5d691868b..ce779972d1 100644 --- a/roles/ferm/tasks/main.yml +++ b/roles/ferm/tasks/main.yml @@ -8,9 +8,8 @@ - name: ensure ferm is installed apt: - pkg: ferm - state: latest - update_cache: true + name: "{{ ferm_package }}" + state: "{{ ferm_package_state | default(apt_security_package_state) }}" cache_valid_time: "{{ apt_cache_valid_time }}" install_recommends: no notify: @@ -20,8 +19,8 @@ file: path: "{{ item }}" state: directory - mode: 0750 - with_items: + mode: '0750' + loop: - /etc/ferm/ferm.d - /etc/ferm/filter-input.d @@ -29,44 +28,34 @@ template: src: "{{ item }}.j2" dest: /{{ item }} - with_items: + mode: '0644' + loop: - etc/default/ferm - etc/ferm/ferm.conf notify: - restart ferm - name: ensure iptables INPUT rules are removed - file: state=absent - {% if item.filename is defined and item.filename %} - path=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.filename }}.conf - {% else %} - path=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.type }}_{{ item.dport[0] }}.conf - {% endif %} - with_flattened: - - "{{ ferm_input_list }}" - - "{{ ferm_input_group_list }}" - - "{{ ferm_input_host_list }}" + file: + path: "/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ (item.filename is defined and item.filename) | ternary(item.filename, item.type + '_' + item.dport[0]) }}.conf" + state: absent + loop: "{{ ferm_input_list + ferm_input_group_list + ferm_input_host_list | flatten}}" when: ((item.type is defined and item.type) and (item.dport is defined and item.dport)) and (item.delete is defined and item.delete) - name: ensure iptables INPUT rules are added - template: src=etc/ferm/filter-input.d/{{ item.type }}.conf.j2 - {% if item.filename is defined and item.filename %} - dest=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.filename }}.conf - {% else %} - dest=/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ item.type }}_{{ item.dport[0] }}.conf - {% endif %} - with_flattened: - - "{{ ferm_input_list }}" - - "{{ ferm_input_group_list }}" - - "{{ ferm_input_host_list }}" + template: + src: "etc/ferm/filter-input.d/{{ item.type }}.conf.j2" + dest: "/etc/ferm/filter-input.d/{{ item.weight | default('50') }}_{{ (item.filename is defined and item.filename) | ternary(item.filename, item.type + '_' + item.dport[0]) }}.conf" + mode: 0644 + loop: "{{ ferm_input_list + ferm_input_group_list + ferm_input_host_list | flatten}}" when: (item.type is defined and item.type and item.dport is defined and item.dport) and (item.delete is undefined or (item.delete is defined and not item.delete)) - name: ensure iptables rules are enabled command: ferm --slow /etc/ferm/ferm.conf changed_when: false - when: ferm_enabled + when: ferm_enabled | bool - name: ensure iptables rules are disabled command: ferm --flush /etc/ferm/ferm.conf diff --git a/roles/hosts/tasks/main.yml b/roles/hosts/tasks/main.yml new file mode 100644 index 0000000000..b8fa6b60fa --- /dev/null +++ b/roles/hosts/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Add WordPress sites to /etc/hosts + lineinfile: + path: /etc/hosts + line: "127.0.0.1 {{ item }}" + create: yes + with_items: "{{ wordpress_sites.values() | map(attribute='site_hosts') | list | flatten | map(attribute='canonical') | list | union(wordpress_sites.values() | map(attribute='site_hosts') | list | flatten | selectattr('redirects', 'defined') | map(attribute='redirects') | list | flatten) }}" \ No newline at end of file diff --git a/roles/letsencrypt/defaults/main.yml b/roles/letsencrypt/defaults/main.yml index b6ad540cc5..4c7ca84aaa 100644 --- a/roles/letsencrypt/defaults/main.yml +++ b/roles/letsencrypt/defaults/main.yml @@ -1,10 +1,16 @@ -sites_using_letsencrypt: "[{% for name, site in wordpress_sites.iteritems() if site.ssl.enabled and site.ssl.provider | default('manual') == 'letsencrypt' %}'{{ name }}',{% endfor %}]" -site_uses_letsencrypt: ssl_enabled and item.value.ssl.provider | default('manual') == 'letsencrypt' +sites_using_letsencrypt: "{{ (wordpress_sites | select_sites('ssl.enabled', 'true') | select_sites('ssl.provider', 'eq', 'letsencrypt')).keys() | list }}" +site_uses_letsencrypt: "{{ (ssl_enabled and item.value.ssl.provider | default('manual') == 'letsencrypt') | bool }}" missing_hosts: "{{ site_hosts | difference((current_hosts.results | selectattr('item.key', 'equalto', item.key) | selectattr('stdout_lines', 'defined') | sum(attribute='stdout_lines', start=[]) | map('trim') | list | join(' ')).split(' ')) }}" -letsencrypt_cert_ids: "{ {% for item in (generate_cert_ids | default({'results':[{'skipped':True}]})).results if not item | skipped %}'{{ item.item.key }}':'{{ item.stdout }}', {% endfor %} }" +letsencrypt_cert_ids: >- + {{ dict((generate_cert_ids | default({'results':[]})).results + | selectattr('stdout', 'defined') + | map(attribute='item.key') + | zip((generate_cert_ids | default({'results':[]})).results + | selectattr('stdout', 'defined') + | map(attribute='stdout'))) }} acme_tiny_repo: 'https://github.com/diafygi/acme-tiny.git' -acme_tiny_commit: '5a7b4e79bc9bd5b51739c0d8aaf644f62cc440e6' +acme_tiny_commit: '1b61d3001cb9c11380557ffebda5d358ce64375c' acme_tiny_software_directory: /usr/local/letsencrypt acme_tiny_data_directory: /var/lib/letsencrypt @@ -24,14 +30,10 @@ acme_tiny_challenges_directory: "{{ www_root }}/letsencrypt" # KWXliiWjUORxDxI1c56Rw2VCIExnFjWJAdSLv6/XaQWo2T7U28bkKbAlCF9= # -----END RSA PRIVATE KEY----- -letsencrypt_ca: 'https://acme-v01.api.letsencrypt.org' +letsencrypt_ca: 'https://acme-v02.api.letsencrypt.org' letsencrypt_account_key: '{{ acme_tiny_data_directory }}/account.key' -letsencrypt_intermediate_cert_path: /etc/ssl/certs/lets-encrypt-x3-cross-signed.pem -letsencrypt_intermediate_cert_url: 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem' -letsencrypt_intermediate_cert_sha256sum: 'e446c5e9dbef9d09ac9f7027c034602492437a05ff6c40011d7235fca639c79a' - letsencrypt_keys_dir: "{{ nginx_ssl_path }}/letsencrypt" letsencrypt_certs_dir: "{{ nginx_ssl_path }}/letsencrypt" diff --git a/roles/letsencrypt/library/test_challenges.py b/roles/letsencrypt/library/test_challenges.py index 8c075cc5a8..f9d2299ddc 100644 --- a/roles/letsencrypt/library/test_challenges.py +++ b/roles/letsencrypt/library/test_challenges.py @@ -1,8 +1,8 @@ -#!/usr/bin/python +#!/usr/bin/python3 # -*- coding: utf-8 -*- import socket -from httplib import HTTPConnection, HTTPException +from http.client import HTTPConnection, HTTPException DOCUMENTATION = ''' --- @@ -43,7 +43,9 @@ def get_status(host, path, file): try: conn = HTTPConnection(host) - conn.request('HEAD', '/{0}/{1}'.format(path, file)) + conn.request('HEAD', '/{0}/{1}'.format(path, file), None, { + 'User-Agent': 'Trellis Ansible test_challenges module' + }) res = conn.getresponse() except (HTTPException, socket.timeout, socket.error): return 0 diff --git a/roles/letsencrypt/tasks/certificates.yml b/roles/letsencrypt/tasks/certificates.yml index 74dd7e5997..eb330a3fa3 100644 --- a/roles/letsencrypt/tasks/certificates.yml +++ b/roles/letsencrypt/tasks/certificates.yml @@ -4,25 +4,34 @@ args: creates: "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" when: site_uses_letsencrypt - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Ensure correct permissions on private keys file: path: "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" - mode: 0600 + mode: '0600' when: site_uses_letsencrypt - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Generate Lets Encrypt certificate IDs shell: | - echo "{{ [site_hosts | join(' '), letsencrypt_ca, acme_tiny_commit, letsencrypt_intermediate_cert_sha256sum] | join('\n') }}" | + set -eo pipefail + echo "{{ [site_hosts | join(' '), letsencrypt_ca, acme_tiny_commit] | join('\n') }}" | cat {{ letsencrypt_account_key }} {{ letsencrypt_keys_dir }}/{{ item.key }}.key - | md5sum | cut -c -7 + args: + executable: /bin/bash register: generate_cert_ids changed_when: false when: site_uses_letsencrypt - with_dict: "{{ wordpress_sites }}" - tags: [wordpress, wordpress-setup, nginx-includes] + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + tags: [wordpress, wordpress-setup, wordpress-setup-nginx, nginx-includes] - name: Generate CSRs shell: "openssl req -new -sha256 -key '{{ letsencrypt_keys_dir }}/{{ item.key }}.key' -subj '/' -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{{ site_hosts | join(',DNS:') }}')) > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}.csr" @@ -30,13 +39,16 @@ executable: /bin/bash creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}.csr" when: site_uses_letsencrypt - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Generate certificate renewal script template: src: renew-certs.py dest: "{{ acme_tiny_data_directory }}/renew-certs.py" - mode: 0700 + mode: '0700' + tags: [wordpress, wordpress-setup, wordpress-setup-nginx, nginx-includes] - name: Generate the certificates command: ./renew-certs.py @@ -45,3 +57,4 @@ register: generate_certs changed_when: generate_certs.stdout is defined and 'Created' in generate_certs.stdout notify: reload nginx + tags: [wordpress, wordpress-setup, wordpress-setup-nginx, nginx-includes] diff --git a/roles/letsencrypt/tasks/main.yml b/roles/letsencrypt/tasks/main.yml index 27c4b86ac7..b65a534087 100644 --- a/roles/letsencrypt/tasks/main.yml +++ b/roles/letsencrypt/tasks/main.yml @@ -8,8 +8,8 @@ cron_file: letsencrypt-certificate-renewal name: letsencrypt certificate renewal user: root - job: cd {{ acme_tiny_data_directory }} && ./renew-certs.py && /usr/sbin/service nginx reload + job: cd {{ acme_tiny_data_directory }} && ./renew-certs.py ; /usr/sbin/service nginx reload day: "{{ letsencrypt_cronjob_daysofmonth }}" - hour: 4 - minute: 30 + hour: "4" + minute: "30" state: present diff --git a/roles/letsencrypt/tasks/nginx.yml b/roles/letsencrypt/tasks/nginx.yml index 298524cc07..3cf03f006f 100644 --- a/roles/letsencrypt/tasks/nginx.yml +++ b/roles/letsencrypt/tasks/nginx.yml @@ -3,6 +3,7 @@ template: src: acme-challenge-location.conf.j2 dest: "{{ nginx_path }}/acme-challenge-location.conf" + mode: '0644' - name: Get list of hosts in current Nginx conf shell: | @@ -11,17 +12,22 @@ register: current_hosts changed_when: false when: site_uses_letsencrypt - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Create needed Nginx confs for challenges template: src: nginx-challenge-site.conf.j2 dest: "{{ nginx_path }}/sites-available/letsencrypt-{{ item.key }}.conf" + mode: '0644' register: challenge_site_confs when: - site_uses_letsencrypt - - missing_hosts | count - with_dict: "{{ wordpress_sites }}" + - missing_hosts | count > 0 + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Enable Nginx sites file: @@ -31,18 +37,20 @@ register: challenge_sites_enabled when: - site_uses_letsencrypt - - missing_hosts | count - with_dict: "{{ wordpress_sites }}" + - missing_hosts | count > 0 + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" notify: disable temporary challenge sites - import_tasks: "{{ playbook_dir }}/roles/common/tasks/reload_nginx.yml" - when: challenge_site_confs | changed or challenge_sites_enabled | changed + when: challenge_site_confs is changed or challenge_sites_enabled is changed - name: Create test Acme Challenge file - shell: touch {{ acme_tiny_challenges_directory }}/ping.txt - args: - creates: "{{ acme_tiny_challenges_directory }}/ping.txt" - warn: false + file: + path: "{{ acme_tiny_challenges_directory }}/ping.txt" + state: touch + mode: '0644' - name: Test Acme Challenges test_challenges: @@ -50,7 +58,9 @@ register: letsencrypt_test_challenges ignore_errors: true when: site_uses_letsencrypt - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Notify of challenge failures fail: @@ -60,5 +70,5 @@ Make sure that a valid DNS record exists for {{ item.failed_hosts | join(', ') }} and that they point to this server's IP. If you don't want these domains in your SSL certificate, then remove them from `site_hosts`. See https://roots.io/trellis/docs/ssl for more details. - when: not item | skipped and letsencrypt_test_challenges | failed - with_items: "{{ letsencrypt_test_challenges.results }}" + when: item is not skipped and item is failed + loop: "{{ letsencrypt_test_challenges.results }}" diff --git a/roles/letsencrypt/tasks/setup.yml b/roles/letsencrypt/tasks/setup.yml index 6c0d6afcc7..b78a7b0d76 100644 --- a/roles/letsencrypt/tasks/setup.yml +++ b/roles/letsencrypt/tasks/setup.yml @@ -4,7 +4,7 @@ mode: "{{ item.mode | default(omit) }}" path: "{{ item.path }}" state: directory - with_items: + loop: - path: "{{ acme_tiny_data_directory }}" mode: '0700' - path: "{{ acme_tiny_data_directory }}/csrs" @@ -12,6 +12,8 @@ - path: "{{ acme_tiny_challenges_directory }}" - path: "{{ letsencrypt_certs_dir }}" mode: '0700' + loop_control: + label: "{{ item.path }}" - name: Clone acme-tiny repository git: @@ -24,12 +26,14 @@ copy: src: "{{ letsencrypt_account_key_source_file }}" dest: "{{ letsencrypt_account_key }}" + mode: '0700' when: letsencrypt_account_key_source_file is defined - name: Copy Lets Encrypt account key source contents copy: content: "{{ letsencrypt_account_key_source_content | trim }}" dest: "{{ letsencrypt_account_key }}" + mode: '0700' when: letsencrypt_account_key_source_content is defined - name: Generate a new account key @@ -37,9 +41,3 @@ args: creates: "{{ letsencrypt_account_key }}" when: letsencrypt_account_key_source_content is not defined and letsencrypt_account_key_source_file is not defined - -- name: Download intermediate certificate - get_url: - url: "{{ letsencrypt_intermediate_cert_url }}" - dest: "{{ letsencrypt_intermediate_cert_path }}" - sha256sum: "{{ letsencrypt_intermediate_cert_sha256sum }}" diff --git a/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 b/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 index 2741378f79..ad476d9d3d 100644 --- a/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 +++ b/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 @@ -1,4 +1,5 @@ server { + listen [::]:80; listen 80; server_name {{ missing_hosts | join(' ') }}; include acme-challenge-location.conf; diff --git a/roles/letsencrypt/templates/renew-certs.py b/roles/letsencrypt/templates/renew-certs.py index 88cd133419..7b19561649 100644 --- a/roles/letsencrypt/templates/renew-certs.py +++ b/roles/letsencrypt/templates/renew-certs.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import os import sys @@ -10,44 +10,51 @@ letsencrypt_cert_ids = {{ letsencrypt_cert_ids }} for site in {{ sites_using_letsencrypt }}: - cert_path = os.path.join('{{ letsencrypt_certs_dir }}', site + '-' + letsencrypt_cert_ids[site] + '.cert') - bundled_cert_path = os.path.join('{{ letsencrypt_certs_dir }}', site + '-' + letsencrypt_cert_ids[site] + '-bundled.cert') + csr_path = os.path.join('{{ acme_tiny_data_directory }}', 'csrs', '{}-{}.csr'.format(site, letsencrypt_cert_ids[site])) + bundled_cert_path = os.path.join('{{ letsencrypt_certs_dir }}', '{}-bundled.cert'.format(site)) + bundled_hashed_cert_path = os.path.join('{{ letsencrypt_certs_dir }}', '{}-{}-bundled.cert'.format(site, letsencrypt_cert_ids[site])) - if os.access(cert_path, os.F_OK): - stat = os.stat(cert_path) - print 'Certificate file ' + cert_path + ' already exists' - - if time.time() - stat.st_mtime < {{ letsencrypt_min_renewal_age }} * 86400: - print ' The certificate is younger than {{ letsencrypt_min_renewal_age }} days. Not creating a new certificate.\n' - continue - - print 'Generating certificate for ' + site + # Generate or update root cert if needed + if not os.access(csr_path, os.F_OK): + failed = True + print('The required CSR file {} does not exist. This could happen if you changed site_hosts and have ' + 'not yet rerun the letsencrypt role. Create the CSR file by re-provisioning (running the Trellis ' + 'server.yml playbook) with `--tags letsencrypt`'.format(csr_path), file=sys.stderr) + continue - cmd = ('/usr/bin/env python {{ acme_tiny_software_directory }}/acme_tiny.py ' - '--quiet ' - '--ca {{ letsencrypt_ca }} ' - '--account-key {{ letsencrypt_account_key }} ' - '--csr {{ acme_tiny_data_directory }}/csrs/{0}-{1}.csr ' - '--acme-dir {{ acme_tiny_challenges_directory }}' - ).format(site, letsencrypt_cert_ids[site]) + elif os.access(bundled_hashed_cert_path, os.F_OK) and time.time() - os.stat(bundled_hashed_cert_path).st_mtime < {{ letsencrypt_min_renewal_age }} * 86400: + print('Certificate file {} already exists and is younger than {{ letsencrypt_min_renewal_age }} days. ' + 'Not creating a new certificate.'.format(bundled_hashed_cert_path)) - try: - cert = check_output(cmd, stderr=STDOUT, shell=True) - except CalledProcessError as e: - failed = True - print 'Error while generating certificate for ' + site - print e.output else: - with open(cert_path, 'w') as cert_file: - cert_file.write(cert) + cmd = ('/usr/bin/env python3 {{ acme_tiny_software_directory }}/acme_tiny.py ' + '--quiet ' + '--ca {{ letsencrypt_ca }} ' + '--account-key {{ letsencrypt_account_key }} ' + '--csr {} ' + '--acme-dir {{ acme_tiny_challenges_directory }}' + ).format(csr_path) + + try: + new_bundled_cert = check_output(cmd, stderr=STDOUT, shell=True, universal_newlines=True) + except CalledProcessError as e: + failed = True + print('Error while generating certificate for {}\n{}'.format(site, e.output), file=sys.stderr) + continue + else: + with open(bundled_hashed_cert_path, 'w') as bundled_hashed_cert_file: + bundled_hashed_cert_file.write(new_bundled_cert) + with open(bundled_cert_path, 'w') as bundled_cert_file: + bundled_cert_file.write(new_bundled_cert) - with open('{{ letsencrypt_intermediate_cert_path }}') as intermediate_cert_file: - intermediate_cert = intermediate_cert_file.read() + if not os.access(bundled_cert_path, os.F_OK): + with open(bundled_hashed_cert_path, 'rb') as bundled_hashed_cert_file: + bundled_hashed_cert = bundled_hashed_cert_file.read() - with open(bundled_cert_path, 'w') as bundled_file: - bundled_file.write(''.join([cert, intermediate_cert])) + with open(bundled_cert_path, 'w') as bundled_cert_file: + bundled_cert_file.write(bundled_hashed_cert) + print('Created bundled certificate {}'.format(bundled_cert_path)) - print 'Created certificate for ' + site if failed: sys.exit(1) diff --git a/roles/mariadb/defaults/main.yml b/roles/mariadb/defaults/main.yml index 95ad282f0c..a163dd33ac 100644 --- a/roles/mariadb/defaults/main.yml +++ b/roles/mariadb/defaults/main.yml @@ -1,4 +1,15 @@ +mariadb_version: 10.11 +mariadb_ppa: "deb https://mirror.rackspace.com/mariadb/repo/{{ mariadb_version }}/ubuntu {{ ansible_distribution_release }} main" + +mariadb_client_package: mariadb-client +mariadb_server_package: mariadb-server + mysql_binary_logging_disabled: true mysql_root_user: root -sites_using_remote_db: "[{% for name, site in wordpress_sites.iteritems() if site.env is defined and site.env.db_host | default('localhost') != 'localhost' %}'{{ name }}',{% endfor %}]" +sites_using_remote_db: "{{ (wordpress_sites | select_sites('env.db_host', 'ne', 'localhost')).keys() | list }}" + +mariadb_set_innodb_buffer_pool_size: false +mariadb_innodb_buffer_pool_size: 128M +mariadb_set_innodb_log_file_size: false +mariadb_innodb_log_file_size: 96M diff --git a/roles/mariadb/tasks/main.yml b/roles/mariadb/tasks/main.yml index 5e998fde95..1c1f978503 100644 --- a/roles/mariadb/tasks/main.yml +++ b/roles/mariadb/tasks/main.yml @@ -1,16 +1,30 @@ --- -- name: Install MySQL client - apt: - name: mariadb-client +- name: Add an Apt signing key, uses whichever key is at the URL + ansible.builtin.apt_key: + url: "https://mariadb.org/mariadb_release_signing_key.asc" state: present + +- name: Add MariaDB PPA + apt_repository: + repo: "{{ mariadb_ppa }}" + update_cache: yes + register: result + until: result is success + retries: 3 + delay: 5 + +- name: Install MySQL client + ansible.builtin.apt: + name: "{{ mariadb_client_package }}" + state: "{{ mariadb_client_package_state | default(apt_package_state) }}" update_cache: true - cache_valid_time: "{{ apt_cache_valid_time }}" - block: - name: Install MySQL server - apt: - name: mariadb-server - state: present + ansible.builtin.apt: + name: "{{ mariadb_server_package }}" + state: "{{ mariadb_server_package_state | default(apt_package_state) }}" + cache_valid_time: "{{ apt_cache_valid_time }}" - name: Disable MariaDB binary logging template: @@ -18,7 +32,23 @@ dest: /etc/mysql/conf.d owner: root group: root - when: mysql_binary_logging_disabled + mode: '0644' + when: mysql_binary_logging_disabled | bool + notify: restart mysql server + + - name: Copy .my.cnf file with root password credentials. + template: + src: my.cnf.j2 + dest: ~/.my.cnf + owner: root + group: root + mode: '0600' + + - name: Copy server config file with MariaDB optimisations. + template: + src: 50-server.cnf.j2 + dest: /etc/mysql/mariadb.conf.d/50-server.cnf + mode: '0644' notify: restart mysql server - name: Set root user password @@ -27,27 +57,24 @@ host: "{{ item }}" password: "{{ mysql_root_password }}" check_implicit_admin: yes + column_case_sensitive: no state: present - with_items: + login_unix_socket: /var/run/mysqld/mysqld.sock + no_log: true + loop: - "{{ inventory_hostname }}" - 127.0.0.1 - ::1 - localhost - - name: Copy .my.cnf file with root password credentials. - template: - src: my.cnf.j2 - dest: ~/.my.cnf - owner: root - group: root - mode: 0600 - - name: Delete anonymous MySQL server users mysql_user: - user: "" + name: "" host: "{{ item }}" state: absent - with_items: + column_case_sensitive: no + no_log: true + loop: - localhost - "{{ inventory_hostname }}" - "{{ ansible_hostname }}" @@ -56,5 +83,6 @@ mysql_db: name: test state: absent + no_log: true - when: not sites_using_remote_db | count + when: (wordpress_sites.items() | count) > (sites_using_remote_db | count) diff --git a/roles/mariadb/templates/50-server.cnf.j2 b/roles/mariadb/templates/50-server.cnf.j2 new file mode 100644 index 0000000000..f46ee20525 --- /dev/null +++ b/roles/mariadb/templates/50-server.cnf.j2 @@ -0,0 +1,132 @@ +# {{ ansible_managed }} +# +# These groups are read by MariaDB server. +# Use it for options that only the server (but not clients) should see + +# this is read by the standalone daemon and embedded servers +[server] + +# this is only for the mysqld standalone daemon +[mysqld] + +# +# * Basic Settings +# + +user = mysql +pid-file = /run/mysqld/mysqld.pid +basedir = /usr +datadir = /var/lib/mysql +tmpdir = /tmp +lc-messages-dir = /usr/share/mysql +lc-messages = en_US +skip-external-locking + +# Broken reverse DNS slows down connections considerably and name resolve is +# safe to skip if there are no "host by domain name" access grants +#skip-name-resolve + +# Instead of skip-networking the default is now to listen only on +# localhost which is more compatible and is not less secure. +bind-address = 127.0.0.1 + +# +# * Fine Tuning +# + +#key_buffer_size = 128M +#max_allowed_packet = 1G +#thread_stack = 192K +#thread_cache_size = 8 +# This replaces the startup script and checks MyISAM tables if needed +# the first time they are touched +#myisam_recover_options = BACKUP +#max_connections = 100 +#table_cache = 64 + +# +# * Logging and Replication +# + +# Both location gets rotated by the cronjob. +# Be aware that this log type is a performance killer. +# Recommend only changing this at runtime for short testing periods if needed! +#general_log_file = /var/log/mysql/mysql.log +#general_log = 1 + +# When running under systemd, error logging goes via stdout/stderr to journald +# and when running legacy init error logging goes to syslog due to +# /etc/mysql/conf.d/mariadb.conf.d/50-mysqld_safe.cnf +# Enable this if you want to have error logging into a separate file +#log_error = /var/log/mysql/error.log +# Enable the slow query log to see queries with especially long duration +#slow_query_log_file = /var/log/mysql/mariadb-slow.log +#long_query_time = 10 +#log_slow_verbosity = query_plan,explain +#log-queries-not-using-indexes +#min_examined_row_limit = 1000 + +# The following can be used as easy to replay backup logs or for replication. +# note: if you are setting up a replication slave, see README.Debian about +# other settings you may need to change. +#server-id = 1 +#log_bin = /var/log/mysql/mysql-bin.log +expire_logs_days = 10 +#max_binlog_size = 100M + +# +# * SSL/TLS +# + +# For documentation, please read +# https://mariadb.com/kb/en/securing-connections-for-client-and-server/ +#ssl-ca = /etc/mysql/cacert.pem +#ssl-cert = /etc/mysql/server-cert.pem +#ssl-key = /etc/mysql/server-key.pem +#require-secure-transport = on + +# +# * Character sets +# + +# MySQL/MariaDB default is Latin1, but in Debian we rather default to the full +# utf8 4-byte character set. See also client.cnf +character-set-server = utf8mb4 +collation-server = utf8mb4_general_ci + +# +# * InnoDB +# + +# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/. +# Read the manual for more InnoDB related options. There are many! +# Most important is to give InnoDB 80 % of the system RAM for buffer use: +# https://mariadb.com/kb/en/innodb-system-variables/#innodb_buffer_pool_size + +# Amount of RAM to allocate to database buffering. +# - Max is 80% of total RAM. i.e. 80% of 8G, 6.4G is the max. +# - On server doing PHP and MySQL, don't allocate more than 50% because the rest needs to go to PHP. +# - Default is 128M +{% if mariadb_set_innodb_buffer_pool_size %} +innodb_buffer_pool_size = {{ mariadb_innodb_buffer_pool_size }} +{% endif %} + +# Amount of disk space to allocate to database redo file +# - Should be 25% of buffer pool size as per MySQL Tuner (https://github.com/major/MySQLTuner-perl) +# - Default is 96M +{% if mariadb_set_innodb_log_file_size %} +innodb_log_file_size = {{ mariadb_innodb_log_file_size }} +{% endif %} + +# this is only for embedded server +[embedded] + +# This group is only read by MariaDB servers, not by MySQL. +# If you use the same .cnf file for MySQL and MariaDB, +# you can put MariaDB-only options here +[mariadb] + +# This group is only read by MariaDB-{{mariadb_version}} servers. +# If you use the same .cnf file for MariaDB of different versions, +# use this group for options that older servers don't understand +[mariadb-{{mariadb_version}}] diff --git a/roles/memcached/defaults/main.yml b/roles/memcached/defaults/main.yml index f5baaece22..88500be54e 100644 --- a/roles/memcached/defaults/main.yml +++ b/roles/memcached/defaults/main.yml @@ -1,6 +1,12 @@ --- memcached_cache_size: 64 -memcached_fs_file_max: 756024 memcached_listen_ip: 127.0.0.1 memcached_max_conn: 1024 memcached_port: 11211 +memcached_port_udp: 0 + +memcached_packages_default: + memcached: "{{ apt_package_state }}" + +memcached_packages_custom: {} +memcached_packages: "{{ memcached_packages_default | combine(php_memcached_packages, memcached_packages_custom) }}" diff --git a/roles/memcached/tasks/main.yml b/roles/memcached/tasks/main.yml index 2e2c5dff41..c9cb0a3b44 100644 --- a/roles/memcached/tasks/main.yml +++ b/roles/memcached/tasks/main.yml @@ -1,26 +1,20 @@ --- - name: Install memcached apt: - name: "{{ item }}" - state: present - update_cache: yes + name: "{{ item.key }}" + state: "{{ item.value }}" cache_valid_time: "{{ apt_cache_valid_time }}" - with_items: - - memcached - - php-memcached + loop: "{{ memcached_packages |dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Copy the client configuration file template: src: memcached.conf.j2 dest: /etc/memcached.conf + mode: '0644' notify: restart memcached -- name: Set the max open file descriptors - sysctl: - name: fs.file-max - value: "{{ memcached_fs_file_max }}" - state: present - - name: Start the memcached service service: name: memcached diff --git a/roles/memcached/templates/memcached.conf.j2 b/roles/memcached/templates/memcached.conf.j2 index b3714fd712..b8755adc2b 100644 --- a/roles/memcached/templates/memcached.conf.j2 +++ b/roles/memcached/templates/memcached.conf.j2 @@ -10,6 +10,11 @@ # information. -d +# modifies the UDP port, defaulting to on. +# UDP is useful for fetching or setting small items, not as useful for manipulating large items. +# Setting this to 0 will disable it, if you're worried. +-U {{ memcached_port_udp }} + # Log memcached's output to /var/log/memcached logfile /var/log/memcached.log diff --git a/roles/nginx/defaults/main.yml b/roles/nginx/defaults/main.yml index f70ca149a8..57b9fb2187 100644 --- a/roles/nginx/defaults/main.yml +++ b/roles/nginx/defaults/main.yml @@ -1,15 +1,14 @@ --- -nginx_ppa: "ppa:nginx/development" +nginx_ppa: "deb http://nginx.org/packages/mainline/ubuntu {{ ansible_distribution_release }} nginx" nginx_package: nginx nginx_conf: nginx.conf.j2 nginx_path: /etc/nginx +nginx_worker_connections: 8000 nginx_logs_root: /var/log/nginx nginx_user: www-data www-data nginx_fastcgi_buffers: 8 8k nginx_fastcgi_buffer_size: 8k nginx_fastcgi_read_timeout: 120s -nginx_sites_confs: - - src: no-default.conf.j2 # Fastcgi cache params nginx_cache_path: /var/cache/nginx diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml index b042455fa0..12686d5334 100644 --- a/roles/nginx/tasks/main.yml +++ b/roles/nginx/tasks/main.yml @@ -1,47 +1,51 @@ --- +- name: Add Nginx APT key + ansible.builtin.apt_key: + url: "https://nginx.org/keys/nginx_signing.key" + state: present + - name: Add Nginx PPA apt_repository: repo: "{{ nginx_ppa }}" update_cache: yes + register: result + until: result is success + retries: 3 + delay: 5 - name: Install Nginx - apt: + ansible.builtin.apt: name: "{{ nginx_package }}" - state: present - force: yes + state: "{{ nginx_package_state | default(apt_package_state) }}" + update_cache: true + +- name: Ensure site directories exist + file: + path: "{{ nginx_path }}/{{ item }}" + state: directory + mode: '0755' + loop: + - sites-available + - sites-enabled - name: Create SSL directory file: - mode: 0700 + mode: '0700' path: "{{ nginx_path }}/ssl" state: directory -- name: Generate strong unique Diffie-Hellman group. - command: openssl dhparam -out dhparams.pem 2048 - args: - chdir: "{{ nginx_path }}/ssl" - creates: "{{ nginx_path }}/ssl/dhparams.pem" - when: wordpress_sites.values() | map(attribute='ssl') | selectattr('enabled') | list | count - notify: reload nginx - tags: [diffie-hellman] - -- name: Grab h5bp/server-configs-nginx - git: - repo: "https://github.com/h5bp/server-configs-nginx.git" - dest: "{{ nginx_path }}/h5bp-server-configs" - version: c5c6602232e0976d9e69d69874aa84d2a2698265 - force: yes - -- name: Move h5bp configs - command: rsync -ac --delete --info=NAME {{ nginx_path }}/h5bp-server-configs/h5bp/ {{ nginx_path }}/h5bp - register: h5bp_nginx_sync - changed_when: h5bp_nginx_sync.stdout != '' +- name: Copy h5bp configs + copy: + src: templates/h5bp + dest: "{{ nginx_path }}" + mode: '0755' notify: reload nginx - name: Create nginx.conf template: src: "{{ nginx_conf }}" dest: "{{ nginx_path }}/nginx.conf" + mode: '0644' notify: reload nginx tags: nginx-includes @@ -51,21 +55,9 @@ state: absent notify: reload nginx -- name: Create Nginx available sites - template: - src: "{{ item.src }}" - dest: "{{ nginx_path }}/sites-available/{{ item.src | basename | regex_replace('.j2$', '') }}" - with_items: "{{ nginx_sites_confs }}" - when: item.enabled | default(true) - notify: reload nginx - tags: nginx-sites - -- name: Enable or disable Nginx sites - file: - path: "{{ nginx_path }}/sites-enabled/{{ item.src | basename | regex_replace('.j2$', '') }}" - src: "{{ nginx_path }}/sites-available/{{ item.src | basename | regex_replace('.j2$', '') }}" - state: "{{ item.enabled | default(true) | ternary('link', 'absent') }}" - force: yes - with_items: "{{ nginx_sites_confs }}" - notify: reload nginx - tags: nginx-sites +- name: Enable Nginx to start on boot + service: + name: nginx + enabled: yes + state: started + use: service diff --git a/roles/nginx/templates/h5bp/directive-only/cache-file-descriptors.conf b/roles/nginx/templates/h5bp/directive-only/cache-file-descriptors.conf new file mode 100644 index 0000000000..ed312c0068 --- /dev/null +++ b/roles/nginx/templates/h5bp/directive-only/cache-file-descriptors.conf @@ -0,0 +1,19 @@ +# This tells Nginx to cache open file handles, "not found" errors, metadata about files and their permissions, etc. +# +# The upside of this is that Nginx can immediately begin sending data when a popular file is requested, +# and will also know to immediately send a 404 if a file is missing on disk, and so on. +# +# However, it also means that the server won't react immediately to changes on disk, which may be undesirable. +# +# In the below configuration, inactive files are released from the cache after 20 seconds, whereas +# active (recently requested) files are re-validated every 30 seconds. +# +# Descriptors will not be cached unless they are used at least 2 times within 20 seconds (the inactive time). +# +# A maximum of the 1000 most recently used file descriptors can be cached at any time. +# +# Production servers with stable file collections will definitely want to enable the cache. +open_file_cache max=1000 inactive=20s; +open_file_cache_valid 30s; +open_file_cache_min_uses 2; +open_file_cache_errors on; diff --git a/roles/nginx/templates/h5bp/directive-only/cache_expiration.conf b/roles/nginx/templates/h5bp/directive-only/cache_expiration.conf new file mode 100644 index 0000000000..865de49bd5 --- /dev/null +++ b/roles/nginx/templates/h5bp/directive-only/cache_expiration.conf @@ -0,0 +1,61 @@ +map $sent_http_content_type $expires { + default 1y; + + # No content + "" off; + + # CSS + ~*text/css 1y; + + # Data interchange + ~*application/atom\+xml 1h; + ~*application/rdf\+xml 1h; + ~*application/rss\+xml 1h; + + ~*application/json 0; + ~*application/ld\+json 0; + ~*application/schema\+json 0; + ~*application/geo\+json 0; + ~*application/xml 0; + ~*text/calendar 0; + ~*text/xml 0; + + # Favicon (cannot be renamed!) and cursor images + ~*image/vnd.microsoft.icon 1w; + ~*image/x-icon 1w; + + # HTML + ~*text/html 0; + + # JavaScript + ~*application/javascript 1y; + ~*application/x-javascript 1y; + ~*text/javascript 1y; + + # Manifest files + ~*application/manifest\+json 1w; + ~*application/x-web-app-manifest\+json 0; + ~*text/cache-manifest 0; + + # Markdown + ~*text/markdown 0; + + # Media files + ~*audio/ 1y; + ~*image/ 1y; + ~*video/ 1y; + + # WebAssembly + ~*application/wasm 1y; + + # Web fonts + ~*font/ 1y; + ~*application/vnd.ms-fontobject 1y; + ~*application/x-font-ttf 1y; + ~*application/x-font-woff 1y; + ~*application/font-woff 1y; + ~*application/font-woff2 1y; + + # Other + ~*text/x-cross-domain-policy 1w; +} diff --git a/roles/nginx/templates/h5bp/directive-only/cross-origin-requests.conf b/roles/nginx/templates/h5bp/directive-only/cross-origin-requests.conf new file mode 100644 index 0000000000..976961fe24 --- /dev/null +++ b/roles/nginx/templates/h5bp/directive-only/cross-origin-requests.conf @@ -0,0 +1,18 @@ +# ---------------------------------------------------------------------- +# | Cross-origin requests | +# ---------------------------------------------------------------------- + +# Allow cross-origin requests. +# +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS +# https://enable-cors.org/ +# https://www.w3.org/TR/cors/ + +# (!) Do not use this without understanding the consequences. +# This will permit access from any other website. +# Instead of using this file, consider using a specific rule such as +# allowing access based on (sub)domain: +# +# add_header Access-Control-Allow-Origin "subdomain.example.com"; + +add_header Access-Control-Allow-Origin $cors; diff --git a/roles/nginx/templates/h5bp/directive-only/extra-security.conf b/roles/nginx/templates/h5bp/directive-only/extra-security.conf new file mode 100644 index 0000000000..2274628166 --- /dev/null +++ b/roles/nginx/templates/h5bp/directive-only/extra-security.conf @@ -0,0 +1,36 @@ +# The X-Frame-Options header indicates whether a browser should be allowed +# to render a page within a frame or iframe. +# add_header X-Frame-Options SAMEORIGIN always; + +# MIME type sniffing security protection +# There are very few edge cases where you wouldn't want this enabled. +add_header X-Content-Type-Options nosniff always; + +# The X-XSS-Protection header is used by Internet Explorer version 8+ +# The header instructs IE to enable its inbuilt anti-cross-site scripting filter. +add_header X-XSS-Protection "1; mode=block" always; + +# Mitigate the risk of cross-site scripting and other content-injection +# attacks. +# +# This can be done by setting a Content Security Policy which permits +# trusted sources of content for your website. +# +# There is no policy that fits all websites, you will have to modify the +# `Content-Security-Policy` directives in the example depending on your needs. +# +# To make your CSP implementation easier, you can use an online CSP header +# generator such as: +# https://report-uri.com/home/generate/ +# +# It is encouraged that you validate your CSP header using a CSP validator +# such as: +# https://csp-evaluator.withgoogle.com +# +# https://www.w3.org/TR/CSP/ +# https://owasp.org/www-project-secure-headers/#content-security-policy +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +# https://developers.google.com/web/fundamentals/security/csp +# https://content-security-policy.com/ + +# add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' https://www.google-analytics.com;" always; diff --git a/roles/nginx/templates/h5bp/directive-only/no-transform.conf b/roles/nginx/templates/h5bp/directive-only/no-transform.conf new file mode 100644 index 0000000000..eda5464472 --- /dev/null +++ b/roles/nginx/templates/h5bp/directive-only/no-transform.conf @@ -0,0 +1,11 @@ +# Prevent mobile network providers from modifying your site +# +# (!) If you are using `ngx_pagespeed`, please note that setting +# the `Cache-Control: no-transform` response header will prevent +# `PageSpeed` from rewriting `HTML` files, and, if +# `pagespeed DisableRewriteOnNoTransform off` is not used, also +# from rewriting other resources. +# +# https://developers.google.com/speed/pagespeed/module/configuration#notransform + +add_header "Cache-Control" "no-transform"; diff --git a/roles/nginx/templates/h5bp/directive-only/ssl.conf b/roles/nginx/templates/h5bp/directive-only/ssl.conf new file mode 100644 index 0000000000..19e62f03b7 --- /dev/null +++ b/roles/nginx/templates/h5bp/directive-only/ssl.conf @@ -0,0 +1,41 @@ +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers EECDH+CHACHA20:EECDH+AES; +ssl_prefer_server_ciphers off; + +# Optimize SSL by caching session parameters for 10 minutes. This cuts down on the number of expensive SSL handshakes. +# The handshake is the most CPU-intensive operation, and by default it is re-negotiated on every new/parallel connection. +# By enabling a cache (of type "shared between all Nginx workers"), we tell the client to re-use the already negotiated state. +# Further optimization can be achieved by raising keepalive_timeout, but that shouldn't be done unless you serve primarily HTTPS. +ssl_session_cache shared:SSL:10m; # a 1mb cache can hold about 4000 sessions, so we can hold 40000 sessions +ssl_session_timeout 24h; + +# SSL buffer size was added in 1.5.9 +#ssl_buffer_size 1400; # 1400 bytes to fit in one MTU + +# Session tickets appeared in version 1.5.9 +# +# nginx does not auto-rotate session ticket keys: only a HUP / restart will do so and +# when a restart is performed the previous key is lost, which resets all previous +# sessions. The fix for this is to setup a manual rotation mechanism: +# http://trac.nginx.org/nginx/changeset/1356a3b9692441e163b4e78be4e9f5a46c7479e9/nginx +# +# Note that you'll have to define and rotate the keys securely by yourself. In absence +# of such infrastructure, consider turning off session tickets: +ssl_session_tickets off; + +# Use a higher keepalive timeout to reduce the need for repeated handshakes +keepalive_timeout 300s; # up from 75 secs default + +# HSTS (HTTP Strict Transport Security) +# This header tells browsers to cache the certificate for a year and to connect exclusively via HTTPS. +#add_header Strict-Transport-Security "max-age=31536000" always; +# This version tells browsers to treat all subdomains the same as this site and to load exclusively over HTTPS +#add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +# This version tells browsers to treat all subdomains the same as this site and to load exclusively over HTTPS +# Recommend is also to use preload service +#add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + +# This default SSL certificate will be served whenever the client lacks support for SNI (Server Name Indication). +# Make it a symlink to the most important certificate you have, so that users of IE 8 and below on WinXP can see your main site without SSL errors. +#ssl_certificate /etc/nginx/default_ssl.crt; +#ssl_certificate_key /etc/nginx/default_ssl.key; diff --git a/roles/nginx/templates/h5bp/directive-only/x-ua-compatible.conf b/roles/nginx/templates/h5bp/directive-only/x-ua-compatible.conf new file mode 100644 index 0000000000..a51bb31d47 --- /dev/null +++ b/roles/nginx/templates/h5bp/directive-only/x-ua-compatible.conf @@ -0,0 +1,2 @@ +# Force the latest IE version +add_header "X-UA-Compatible" "IE=Edge"; diff --git a/roles/nginx/templates/h5bp/location/cache-busting.conf b/roles/nginx/templates/h5bp/location/cache-busting.conf new file mode 100644 index 0000000000..6afe34abf9 --- /dev/null +++ b/roles/nginx/templates/h5bp/location/cache-busting.conf @@ -0,0 +1,10 @@ +# Built-in filename-based cache busting + +# https://github.com/h5bp/html5-boilerplate/blob/5370479476dceae7cc3ea105946536d6bc0ee468/.htaccess#L403 +# This will route all requests for /css/style.20120716.css to /css/style.css +# Read also this: github.com/h5bp/html5-boilerplate/wiki/cachebusting +# This is not included by default, because it'd be better if you use the build +# script to manage the file names. +location ~* (.+)\.(?:\d+)\.(js|css|png|jpg|jpeg|gif)$ { + try_files $uri $1.$2; +} diff --git a/roles/nginx/templates/h5bp/location/cross-domain-fonts.conf b/roles/nginx/templates/h5bp/location/cross-domain-fonts.conf new file mode 100644 index 0000000000..e0fa318f46 --- /dev/null +++ b/roles/nginx/templates/h5bp/location/cross-domain-fonts.conf @@ -0,0 +1,12 @@ +# Cross domain webfont access +location ~* \.(?:ttf|ttc|otf|eot|woff|woff2)$ { + include h5bp/directive-only/cross-domain-insecure.conf; + + # Also, set cache rules for webfonts. + # + # See http://wiki.nginx.org/HttpCoreModule#location + # And https://github.com/h5bp/server-configs/issues/85 + # And https://github.com/h5bp/server-configs/issues/86 + access_log off; + add_header Cache-Control "max-age=2592000"; +} diff --git a/roles/nginx/templates/h5bp/location/protect-system-files.conf b/roles/nginx/templates/h5bp/location/protect-system-files.conf new file mode 100644 index 0000000000..128c49ab0b --- /dev/null +++ b/roles/nginx/templates/h5bp/location/protect-system-files.conf @@ -0,0 +1,13 @@ +# Prevent clients from accessing hidden files (starting with a dot) +# This is particularly important if you store .htpasswd files in the site hierarchy +# Access to `/.well-known/` is allowed. +# https://www.mnot.net/blog/2010/04/07/well-known +# https://tools.ietf.org/html/rfc5785 +location ~* /\.(?!well-known\/) { + deny all; +} + +# Prevent clients from accessing to backup/config/source files +location ~* (?:\.(?:bak|conf|dist|fla|in[ci]|log|psd|sh|sql|sw[op])|~)$ { + deny all; +} diff --git a/roles/nginx/templates/h5bp/mime.types b/roles/nginx/templates/h5bp/mime.types new file mode 100644 index 0000000000..03dde44898 --- /dev/null +++ b/roles/nginx/templates/h5bp/mime.types @@ -0,0 +1,139 @@ +types { + + # Data interchange + + application/atom+xml atom; + application/json json map topojson; + application/ld+json jsonld; + application/rss+xml rss; + # Normalize to standard type. + # https://tools.ietf.org/html/rfc7946#section-12 + application/geo+json geojson; + application/xml xml; + # Normalize to standard type. + # https://tools.ietf.org/html/rfc3870#section-2 + application/rdf+xml rdf; + + + # JavaScript + + # Servers should use text/javascript for JavaScript resources. + # https://html.spec.whatwg.org/multipage/scripting.html#scriptingLanguages + text/javascript js mjs; + application/wasm wasm; + + + # Manifest files + + application/manifest+json webmanifest; + application/x-web-app-manifest+json webapp; + text/cache-manifest appcache; + + + # Media files + + audio/midi mid midi kar; + audio/mp4 aac f4a f4b m4a; + audio/mpeg mp3; + audio/ogg oga ogg opus; + audio/x-realaudio ra; + audio/x-wav wav; + image/apng apng; + image/avif avif avifs; + image/bmp bmp; + image/gif gif; + image/jpeg jpeg jpg; + image/jxl jxl; + image/jxr jxr hdp wdp; + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-jng jng; + video/3gpp 3gp 3gpp; + video/mp4 f4p f4v m4v mp4; + video/mpeg mpeg mpg; + video/ogg ogv; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-mng mng; + video/x-ms-asf asf asx; + video/x-msvideo avi; + + # Serving `.ico` image files with a different media type + # prevents Internet Explorer from displaying then as images: + # https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee + + image/x-icon cur ico; + + + # Microsoft Office + + application/msword doc; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; + application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; + + + # Web fonts + + font/woff woff; + font/woff2 woff2; + application/vnd.ms-fontobject eot; + font/ttf ttf; + font/collection ttc; + font/otf otf; + + + # Other + + application/java-archive ear jar war; + application/mac-binhex40 hqx; + application/octet-stream bin deb dll dmg exe img iso msi msm msp safariextz; + application/pdf pdf; + application/postscript ai eps ps; + application/rtf rtf; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.wap.wmlc wmlc; + application/x-7z-compressed 7z; + application/x-bb-appworld bbaw; + application/x-bittorrent torrent; + application/x-chrome-extension crx; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-opera-extension oex; + application/x-perl pl pm; + application/x-pilot pdb prc; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert crt der pem; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xslt+xml xsl; + application/zip zip; + text/calendar ics; + text/css css; + text/csv csv; + text/html htm html shtml; + text/markdown md markdown; + text/mathml mml; + text/plain txt; + text/vcard vcard vcf; + text/vnd.rim.location.xloc xloc; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/vtt vtt; + text/x-component htc; + +} diff --git a/roles/nginx/templates/nginx.conf.j2 b/roles/nginx/templates/nginx.conf.j2 index 5a587d5434..9bf734f535 100644 --- a/roles/nginx/templates/nginx.conf.j2 +++ b/roles/nginx/templates/nginx.conf.j2 @@ -32,7 +32,7 @@ events { # That's probably the point at which you hire people who are smarter than you as this is *a lot* of requests. # Should be < worker_rlimit_nofile. # Default: 512 - worker_connections 8000; + worker_connections {{ nginx_worker_connections }}; } {% endblock %} @@ -50,7 +50,22 @@ pid /run/nginx.pid; {% endblock %} http { - {% block http_begin %}{% endblock %} + {% block http_begin %} + map $upstream_cache_status $header_x_cache_enabled { + default true; + BYPASS ""; + } + + map $server_addr:$remote_addr $is_loopback_request { + "~^([^:]+):\1$" 1; + default 0; + } + + map $is_loopback_request:$header_x_cache_enabled $loopback_header_x_cache_enabled { + default ""; + 1:true true; + } + {% endblock %} {% block server_tokens -%} # Hide nginx version information. @@ -66,15 +81,15 @@ http { fastcgi_cache_path {{ nginx_cache_path }} levels=1:2 keys_zone=wordpress:{{ nginx_cache_key_storage_size }} max_size={{ nginx_cache_size }} inactive={{ nginx_cache_inactive }}; fastcgi_cache_use_stale updating error timeout invalid_header http_500; fastcgi_cache_lock on; - fastcgi_cache_key $realpath_root$scheme$host$request_uri$request_method$http_origin; - fastcgi_ignore_headers Cache-Control Expires Set-Cookie; + fastcgi_cache_key $realpath_root$scheme$host$request_uri$request_method$http_origin$http_x_http_method_override; + fastcgi_ignore_headers Expires Set-Cookie; fastcgi_pass_header Set-Cookie; fastcgi_pass_header Cookie; {% endblock %} {% block mime_types -%} # Specify MIME types for files. - include h5bp-server-configs/mime.types; + include h5bp/mime.types; # Default: text/plain default_type application/octet-stream; @@ -186,6 +201,7 @@ http { image/x-icon text/cache-manifest text/css + text/javascript text/plain text/vcard text/vnd.rim.location.xloc @@ -199,11 +215,40 @@ http { # a specific directory, or on an individual server{} level. # gzip_static on; {% endblock %} - + {% block http_includes_d -%} include includes.d/http/*.conf; {% endblock -%} + # Add Access-Control-Allow-Origin. + # h5bp/directive-only/cross-origin-requests.conf + map $sent_http_content_type $cors { + # Images + image/bmp "*"; + image/gif "*"; + image/jpeg "*"; + image/png "*"; + image/svg+xml "*"; + image/webp "*"; + image/x-icon "*"; + + # Web fonts + font/collection "*"; + application/vnd.ms-fontobject "*"; + font/eot "*"; + font/opentype "*"; + font/otf "*"; + application/x-font-ttf "*"; + font/ttf "*"; + application/font-woff "*"; + application/x-font-woff "*"; + font/woff "*"; + application/font-woff2 "*"; + font/woff2 "*"; + } + + include h5bp/directive-only/cache_expiration.conf; + {% block sites_enabled -%} # Include files in the sites-enabled folder. server{} configuration files should be # placed in the sites-available folder, and then the configuration should be enabled diff --git a/roles/php/defaults/main.yml b/roles/php/defaults/main.yml index 518b6491b1..a807a80da4 100644 --- a/roles/php/defaults/main.yml +++ b/roles/php/defaults/main.yml @@ -1,23 +1,8 @@ disable_default_pool: true memcached_sessions: false -php_extensions_default: - - php7.1-cli - - php7.1-common - - php7.1-curl - - php7.1-dev - - php7.1-fpm - - php7.1-gd - - php7.1-mbstring - - php7.1-mcrypt - - php7.1-mysql - - php7.1-opcache - - php7.1-xml - - php7.1-xmlrpc - - php7.1-zip - -php_extensions_custom: [] -php_extensions: "{{ php_extensions_default + php_extensions_custom }}" +php_extensions_custom: {} +php_extensions: "{{ php_extensions_default | combine(php_extensions_custom) }}" php_error_reporting: 'E_ALL & ~E_DEPRECATED & ~E_STRICT' php_display_errors: 'Off' @@ -26,6 +11,7 @@ php_max_execution_time: 120 php_max_input_time: 300 php_max_input_vars: 1000 php_memory_limit: 96M +php_cli_memory_limit: -1 php_mysqlnd_collect_memory_statistics: 'Off' php_post_max_size: 25M php_sendmail_path: /usr/sbin/ssmtp -t @@ -33,13 +19,26 @@ php_session_save_path: /tmp php_session_cookie_httponly: 'On' php_session_cookie_secure: 'Off' php_upload_max_filesize: 25M -php_track_errors: 'Off' php_timezone: '{{ ntp_timezone }}' +php_output_buffering: 'Off' php_opcache_enable: 1 php_opcache_enable_cli: 1 +php_opcache_enable_file_override: 0 php_opcache_fast_shutdown: 1 php_opcache_interned_strings_buffer: 8 php_opcache_max_accelerated_files: 4000 php_opcache_memory_consumption: 128 php_opcache_revalidate_freq: 60 +php_opcache_validate_timestamps: 1 +php_opcache_max_wasted_percentage: 5 +php_opcache_huge_code_pages: 0 +php_opcache_jit: 'tracing' +php_opcache_jit_buffer_size: 256M + +php_fpm_set_emergency_restart_threshold: false +php_fpm_emergency_restart_threshold: 0 +php_fpm_set_emergency_restart_interval: false +php_fpm_emergency_restart_interval: 0 +php_fpm_set_process_control_timeout: true +php_fpm_process_control_timeout: 10 diff --git a/roles/php/tasks/main.yml b/roles/php/tasks/main.yml index c88a1f10a2..164aeffdb3 100644 --- a/roles/php/tasks/main.yml +++ b/roles/php/tasks/main.yml @@ -1,24 +1,81 @@ --- -- name: Add PHP 7.1 PPA +- name: Add PHP PPA apt_repository: repo: "ppa:ondrej/php" update_cache: yes + register: result + until: result is success + retries: 3 + delay: 5 -- name: Install PHP 7.1 +- name: Install PHP and extensions apt: - name: "{{ item }}" - state: present - force: yes - with_items: "{{ php_extensions }}" + name: "{{ item.key }}" + state: "{{ item.value }}" + cache_valid_time: "{{ apt_cache_valid_time }}" + install_recommends: no + loop: "{{ php_extensions | dict2items }}" + loop_control: + label: "{{ item.key }}" -- name: Start php7.1-fpm service +- name: Ensure correct PHP version selected + community.general.alternatives: + name: php + path: /usr/bin/php{{ php_version }} + +- name: Find existing php fpm services + find: + paths: /etc/init.d + patterns: "^php((?!{{ php_version }})(\\d\\.\\d))-fpm$" + use_regex: true + register: old_php_fpm_services + +- name: Stop old php-fpm services service: - name: php7.1-fpm + name: "{{ item.path | basename }}" + state: stopped + enabled: false + loop: "{{ old_php_fpm_services.files }}" + loop_control: + label: "{{ item.path | basename }}" + notify: reload php-fpm + +- name: Start php fpm service + service: + name: "php{{ php_version }}-fpm" state: started enabled: true -- name: PHP configuration file +- name: Copy PHP-FPM php.ini file + template: + src: php-fpm.ini.j2 + dest: /etc/php/{{ php_version }}/fpm/php.ini + mode: '0644' + +- name: Copy PHP-FPM configuration file template: - src: php.ini.j2 - dest: /etc/php/7.1/fpm/php.ini + src: php-fpm.conf.j2 + dest: /etc/php/{{ php_version }}/fpm/php-fpm.conf + mode: '0644' + notify: reload php-fpm + +- name: Copy PHP CLI configuration file + template: + src: php-cli.ini.j2 + dest: /etc/php/{{ php_version }}/cli/php.ini + mode: '0644' + +- name: Copy 10-opcache.ini configuration file + template: + src: 10-opcache.ini.j2 + dest: /etc/php/{{ php_version }}/fpm/conf.d/10-opcache.ini + mode: '0644' + notify: reload php-fpm + +- name: Change ImageMagick policy.xml to allow for PDFs + replace: + path: /etc/ImageMagick-6/policy.xml + regexp: '<policy domain="coder" rights="none" pattern="PDF" />' + replace: '<policy domain="coder" rights="read" pattern="PDF" />' + backup: no notify: reload php-fpm diff --git a/roles/php/templates/10-opcache.ini.j2 b/roles/php/templates/10-opcache.ini.j2 new file mode 100644 index 0000000000..5f5f74edb7 --- /dev/null +++ b/roles/php/templates/10-opcache.ini.j2 @@ -0,0 +1,6 @@ +; {{ ansible_managed }} + +; configuration for php opcache module +; priority=10 +zend_extension=opcache.so +opcache.jit={{ php_opcache_jit }} diff --git a/roles/php/templates/php-cli.ini.j2 b/roles/php/templates/php-cli.ini.j2 new file mode 100644 index 0000000000..44360aa80d --- /dev/null +++ b/roles/php/templates/php-cli.ini.j2 @@ -0,0 +1,20 @@ +; {{ ansible_managed }} + +[PHP] +error_reporting = {{ php_error_reporting }} +sendmail_path = {{ php_sendmail_path }} +expose_php = Off +date.timezone = {{ php_timezone }} +memory_limit = {{ php_cli_memory_limit }} + +[mysqlnd] +mysqlnd.collect_memory_statistics = {{ php_mysqlnd_collect_memory_statistics }} + +[opcache] +opcache.enable = {{ php_opcache_enable }} +opcache.enable_cli = {{ php_opcache_enable_cli }} +opcache.memory_consumption = {{ php_opcache_memory_consumption }} +opcache.interned_strings_buffer = {{ php_opcache_interned_strings_buffer }} +opcache.max_accelerated_files = {{ php_opcache_max_accelerated_files }} +opcache.revalidate_freq = {{ php_opcache_revalidate_freq }} +opcache.fast_shutdown = {{ php_opcache_fast_shutdown }} diff --git a/roles/php/templates/php-fpm.conf.j2 b/roles/php/templates/php-fpm.conf.j2 new file mode 100644 index 0000000000..0c86ca4a1c --- /dev/null +++ b/roles/php/templates/php-fpm.conf.j2 @@ -0,0 +1,153 @@ +; {{ ansible_managed }} + +;;;;;;;;;;;;;;;;;;;;; +; FPM Configuration ; +;;;;;;;;;;;;;;;;;;;;; + +; All relative paths in this configuration file are relative to PHP's install +; prefix (/usr). This prefix can be dynamically changed by using the +; '-p' argument from the command line. + +;;;;;;;;;;;;;;;;;; +; Global Options ; +;;;;;;;;;;;;;;;;;; + +[global] +; Pid file +; Note: the default prefix is /var +; Default Value: none +; Warning: if you change the value here, you need to modify systemd +; service PIDFile= setting to match the value here. +pid = /run/php/php{{ php_version }}-fpm.pid + +; Error log file +; If it's set to "syslog", log is sent to syslogd instead of being written +; into a local file. +; Note: the default prefix is /var +; Default Value: log/php-fpm.log +error_log = /var/log/php{{ php_version }}-fpm.log + +; syslog_facility is used to specify what type of program is logging the +; message. This lets syslogd specify that messages from different facilities +; will be handled differently. +; See syslog(3) for possible values (ex daemon equiv LOG_DAEMON) +; Default Value: daemon +;syslog.facility = daemon + +; syslog_ident is prepended to every message. If you have multiple FPM +; instances running on the same server, you can change the default value +; which must suit common needs. +; Default Value: php-fpm +;syslog.ident = php-fpm + +; Log level +; Possible Values: alert, error, warning, notice, debug +; Default Value: notice +log_level = notice + +; Log limit on number of characters in the single line (log entry). If the +; line is over the limit, it is wrapped on multiple lines. The limit is for +; all logged characters including message prefix and suffix if present. However +; the new line character does not count into it as it is present only when +; logging to a file descriptor. It means the new line character is not present +; when logging to syslog. +; Default Value: 1024 +;log_limit = 4096 + +; Log buffering specifies if the log line is buffered which means that the +; line is written in a single write operation. If the value is false, then the +; data is written directly into the file descriptor. It is an experimental +; option that can potentionaly improve logging performance and memory usage +; for some heavy logging scenarios. This option is ignored if logging to syslog +; as it has to be always buffered. +; Default value: yes +;log_buffering = no + +; If this number of child processes exit with SIGSEGV or SIGBUS within the time +; interval set by emergency_restart_interval then FPM will restart. A value +; of '0' means 'Off'. +; Default Value: 0 +{% if php_fpm_set_emergency_restart_threshold %} +emergency_restart_threshold = {{ php_fpm_emergency_restart_threshold }} +{% endif %} + +; Interval of time used by emergency_restart_interval to determine when +; a graceful restart will be initiated. This can be useful to work around +; accidental corruptions in an accelerator's shared memory. +; Available Units: s(econds), m(inutes), h(ours), or d(ays) +; Default Unit: seconds +; Default Value: 0 +{% if php_fpm_set_emergency_restart_interval %} +emergency_restart_interval = {{ php_fpm_emergency_restart_interval }} +{% endif %} + +; Time limit for child processes to wait for a reaction on signals from master. +; Available units: s(econds), m(inutes), h(ours), or d(ays) +; Default Unit: seconds +; Default Value: 0 +{% if php_fpm_set_process_control_timeout %} +process_control_timeout = {{ php_fpm_process_control_timeout }} +{% endif %} + +; The maximum number of processes FPM will fork. This has been designed to control +; the global number of processes when using dynamic PM within a lot of pools. +; Use it with caution. +; Note: A value of 0 indicates no limit +; Default Value: 0 +; process.max = 128 + +; Specify the nice(2) priority to apply to the master process (only if set) +; The value can vary from -19 (highest priority) to 20 (lowest priority) +; Note: - It will only work if the FPM master process is launched as root +; - The pool process will inherit the master process priority +; unless specified otherwise +; Default Value: no set +; process.priority = -19 + +; Send FPM to background. Set to 'no' to keep FPM in foreground for debugging. +; Default Value: yes +;daemonize = yes + +; Set open file descriptor rlimit for the master process. +; Default Value: system defined value +;rlimit_files = 1024 + +; Set max core size rlimit for the master process. +; Possible Values: 'unlimited' or an integer greater or equal to 0 +; Default Value: system defined value +;rlimit_core = 0 + +; Specify the event mechanism FPM will use. The following is available: +; - select (any POSIX os) +; - poll (any POSIX os) +; - epoll (linux >= 2.5.44) +; - kqueue (FreeBSD >= 4.1, OpenBSD >= 2.9, NetBSD >= 2.0) +; - /dev/poll (Solaris >= 7) +; - port (Solaris >= 10) +; Default Value: not set (auto detection) +;events.mechanism = epoll + +; When FPM is built with systemd integration, specify the interval, +; in seconds, between health report notification to systemd. +; Set to 0 to disable. +; Available Units: s(econds), m(inutes), h(ours) +; Default Unit: seconds +; Default value: 10 +;systemd_interval = 10 + +;;;;;;;;;;;;;;;;;;;; +; Pool Definitions ; +;;;;;;;;;;;;;;;;;;;; + +; Multiple pools of child processes may be started with different listening +; ports and different management options. The name of the pool will be +; used in logs and stats. There is no limitation on the number of pools which +; FPM can handle. Your system will tell you anyway :) + +; Include one or more files. If glob(3) exists, it is used to include a bunch of +; files from a glob(3) pattern. This directive can be used everywhere in the +; file. +; Relative path can also be used. They will be prefixed by: +; - the global prefix if it's been set (-p argument) +; - /usr otherwise +include=/etc/php/{{ php_version }}/fpm/pool.d/*.conf diff --git a/roles/php/templates/php.ini.j2 b/roles/php/templates/php-fpm.ini.j2 similarity index 76% rename from roles/php/templates/php.ini.j2 rename to roles/php/templates/php-fpm.ini.j2 index 3b899e7c91..d07cfc9b62 100644 --- a/roles/php/templates/php.ini.j2 +++ b/roles/php/templates/php-fpm.ini.j2 @@ -13,10 +13,10 @@ sendmail_path = {{ php_sendmail_path }} session.save_path = {{ php_session_save_path }} session.cookie_httponly = {{ php_session_cookie_httponly }} session.cookie_secure = {{ php_session_cookie_secure }} -track_errors = {{ php_track_errors }} upload_max_filesize = {{ php_upload_max_filesize }} expose_php = Off date.timezone = {{ php_timezone }} +output_buffering = {{ php_output_buffering }} [mysqlnd] mysqlnd.collect_memory_statistics = {{ php_mysqlnd_collect_memory_statistics }} @@ -27,5 +27,10 @@ opcache.enable_cli = {{ php_opcache_enable_cli }} opcache.memory_consumption = {{ php_opcache_memory_consumption }} opcache.interned_strings_buffer = {{ php_opcache_interned_strings_buffer }} opcache.max_accelerated_files = {{ php_opcache_max_accelerated_files }} +opcache.validate_timestamps = {{ php_opcache_validate_timestamps }} +opcache.enable_file_override = {{ php_opcache_enable_file_override }} opcache.revalidate_freq = {{ php_opcache_revalidate_freq }} opcache.fast_shutdown = {{ php_opcache_fast_shutdown }} +opcache.max_wasted_percentage = {{ php_opcache_max_wasted_percentage }} +opcache.huge_code_pages = {{ php_opcache_huge_code_pages }} +opcache.jit_buffer_size = {{ php_opcache_jit_buffer_size }} diff --git a/roles/php/vars/version-specific-defaults.yml b/roles/php/vars/version-specific-defaults.yml new file mode 100644 index 0000000000..8d1bfb2e69 --- /dev/null +++ b/roles/php/vars/version-specific-defaults.yml @@ -0,0 +1,32 @@ +# +# Set php version in group_vars/all/main.yml +# +# To override these defaults for a specific php version, duplicate this +# file to roles/php/vars/ and rename with your specific php version +# e.g. roles/php/vars/8.2.yml. +# +# You can then use e.g. +# php_extensions_default: +# php8.2-bcmath: "{{ apt_package_state }}" +# +php_extensions_default: + "php{{ php_version }}-bcmath": "{{ apt_package_state }}" + "php{{ php_version }}-cli": "{{ apt_package_state }}" + "php{{ php_version }}-curl": "{{ apt_package_state }}" + "php{{ php_version }}-dev": "{{ apt_package_state }}" + "php{{ php_version }}-fpm": "{{ apt_package_state }}" + "php{{ php_version }}-imagick": "{{ apt_package_state }}" + "php{{ php_version }}-intl": "{{ apt_package_state }}" + "php{{ php_version }}-mbstring": "{{ apt_package_state }}" + "php{{ php_version }}-mysql": "{{ apt_package_state }}" + "php{{ php_version }}-xml": "{{ apt_package_state }}" + "php{{ php_version }}-xmlrpc": "{{ apt_package_state }}" + "php{{ php_version }}-zip": "{{ apt_package_state }}" + +php_memcached_packages: + "php{{ php_version }}-memcached": "{{ apt_package_state }}" + +php_redis_packages: + "php{{ php_version }}-redis": "{{ apt_package_state }}" + +php_xdebug_package: "php{{ php_version }}-xdebug" diff --git a/roles/redis/defaults/main.yml b/roles/redis/defaults/main.yml new file mode 100644 index 0000000000..f1355fa74c --- /dev/null +++ b/roles/redis/defaults/main.yml @@ -0,0 +1,87 @@ +--- +# Basic Redis configuration +redis_daemonize: "yes" +redis_pidfile: /run/redis/redis-server.pid +redis_port: 6379 +redis_bind_interface: 127.0.0.1 +redis_unixsocket: /var/run/redis/redis.sock +redis_timeout: 0 +redis_tcp_keepalive: 0 + +# Logging +redis_loglevel: notice +redis_logfile: /var/log/redis/redis-server.log + +# Database configuration +redis_databases: 16 + +# Persistence - RDB snapshots +redis_save: + - "900 1" + - "300 10" + - "60 10000" + +redis_stop_writes_on_bgsave_error: "yes" +redis_rdbcompression: "yes" +redis_rdbchecksum: "yes" +redis_dbfilename: dump.rdb +redis_dir: /var/lib/redis + +# Memory management +redis_maxclients: 10000 +redis_maxmemory: 256mb +redis_maxmemory_policy: allkeys-lru +redis_maxmemory_samples: 5 + +# Append Only File (AOF) persistence +redis_appendonly: "no" +redis_appendfilename: appendonly.aof +redis_appendfsync: everysec +redis_no_appendfsync_on_rewrite: "no" +redis_auto_aof_rewrite_percentage: 100 +redis_auto_aof_rewrite_min_size: 64mb +redis_aof_load_truncated: "yes" + +# Advanced configuration +redis_lua_time_limit: 5000 +redis_slowlog_log_slower_than: 10000 +redis_slowlog_max_len: 128 +redis_latency_monitor_threshold: 0 + +# Hash table optimizations +redis_hash_max_ziplist_entries: 512 +redis_hash_max_ziplist_value: 64 +redis_list_max_ziplist_entries: 512 +redis_list_max_ziplist_value: 64 +redis_set_max_intset_entries: 512 +redis_zset_max_ziplist_entries: 128 +redis_zset_max_ziplist_value: 64 +redis_hll_sparse_max_bytes: 3000 +redis_activerehashing: "yes" + +# Client output buffer limits +redis_client_output_buffer_limit_normal: "0 0 0" +redis_client_output_buffer_limit_replica: "256mb 64mb 60" +redis_client_output_buffer_limit_pubsub: "32mb 8mb 60" + +# Misc +redis_hz: 10 +redis_aof_rewrite_incremental_fsync: "yes" + +# Security +redis_requirepass: false +redis_disabled_commands: + - FLUSHALL + - KEYS + - CONFIG + - SHUTDOWN + +# Custom configuration +redis_includes: [] +redis_extra_config: {} + +redis_packages_default: + redis-server: "{{ apt_package_state }}" + +redis_packages_custom: {} +redis_packages: "{{ redis_packages_default | combine(php_redis_packages, redis_packages_custom) }}" diff --git a/roles/redis/handlers/main.yml b/roles/redis/handlers/main.yml new file mode 100644 index 0000000000..d3d79a6b91 --- /dev/null +++ b/roles/redis/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart redis + service: + name: redis-server + state: restarted \ No newline at end of file diff --git a/roles/redis/tasks/main.yml b/roles/redis/tasks/main.yml new file mode 100644 index 0000000000..f7b17ca1eb --- /dev/null +++ b/roles/redis/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: Install Redis and PHP extension + apt: + name: "{{ item.key }}" + state: "{{ item.value }}" + update_cache: true + cache_valid_time: "{{ apt_cache_valid_time }}" + loop: "{{ redis_packages | dict2items }}" + loop_control: + label: "{{ item.key }}" + +- name: Create Redis configuration file + template: + src: redis.conf.j2 + dest: /etc/redis/redis.conf + owner: redis + group: redis + mode: 0640 + notify: restart redis + +- name: Create Redis socket directory + file: + path: /var/run/redis + state: directory + owner: redis + group: redis + mode: 0755 + when: redis_unixsocket is defined + +- name: Add web user to redis group for socket access + user: + name: "{{ web_user }}" + groups: redis + append: true + when: redis_unixsocket is defined + +- name: Ensure Redis is started and enabled + service: + name: redis-server + state: started + enabled: true \ No newline at end of file diff --git a/roles/redis/templates/redis.conf.j2 b/roles/redis/templates/redis.conf.j2 new file mode 100644 index 0000000000..cc8dff18e2 --- /dev/null +++ b/roles/redis/templates/redis.conf.j2 @@ -0,0 +1,105 @@ +# {{ ansible_managed }} + +# Network configuration +bind {{ redis_bind_interface }} +port {{ redis_port }} + +{% if redis_unixsocket %} +unixsocket {{ redis_unixsocket }} +unixsocketperm 770 +{% endif %} + +# General configuration +daemonize {{ redis_daemonize }} +pidfile {{ redis_pidfile }} +timeout {{ redis_timeout }} +tcp-keepalive {{ redis_tcp_keepalive }} + +# Logging +loglevel {{ redis_loglevel }} +logfile {{ redis_logfile }} + +# Database configuration +databases {{ redis_databases }} + +# RDB persistence +{% for save in redis_save %} +save {{ save }} +{% endfor %} + +stop-writes-on-bgsave-error {{ redis_stop_writes_on_bgsave_error }} +rdbcompression {{ redis_rdbcompression }} +rdbchecksum {{ redis_rdbchecksum }} +dbfilename {{ redis_dbfilename }} +dir {{ redis_dir }} + +# Memory management +maxclients {{ redis_maxclients }} +{% if redis_maxmemory %} +maxmemory {{ redis_maxmemory }} +maxmemory-policy {{ redis_maxmemory_policy }} +maxmemory-samples {{ redis_maxmemory_samples }} +{% endif %} + +# AOF persistence +appendonly {{ redis_appendonly }} +appendfilename {{ redis_appendfilename }} +appendfsync {{ redis_appendfsync }} +no-appendfsync-on-rewrite {{ redis_no_appendfsync_on_rewrite }} +auto-aof-rewrite-percentage {{ redis_auto_aof_rewrite_percentage }} +auto-aof-rewrite-min-size {{ redis_auto_aof_rewrite_min_size }} +aof-load-truncated {{ redis_aof_load_truncated }} + +# Lua scripting +lua-time-limit {{ redis_lua_time_limit }} + +# Slow log +slowlog-log-slower-than {{ redis_slowlog_log_slower_than }} +slowlog-max-len {{ redis_slowlog_max_len }} + +# Latency monitoring +latency-monitor-threshold {{ redis_latency_monitor_threshold }} + +# Event notification +notify-keyspace-events "" + +# Hash table optimizations +hash-max-ziplist-entries {{ redis_hash_max_ziplist_entries }} +hash-max-ziplist-value {{ redis_hash_max_ziplist_value }} +list-max-ziplist-entries {{ redis_list_max_ziplist_entries }} +list-max-ziplist-value {{ redis_list_max_ziplist_value }} +set-max-intset-entries {{ redis_set_max_intset_entries }} +zset-max-ziplist-entries {{ redis_zset_max_ziplist_entries }} +zset-max-ziplist-value {{ redis_zset_max_ziplist_value }} +hll-sparse-max-bytes {{ redis_hll_sparse_max_bytes }} + +# Rehashing +activerehashing {{ redis_activerehashing }} + +# Client output buffer limits +client-output-buffer-limit normal {{ redis_client_output_buffer_limit_normal }} +client-output-buffer-limit replica {{ redis_client_output_buffer_limit_replica }} +client-output-buffer-limit pubsub {{ redis_client_output_buffer_limit_pubsub }} + +# Miscellaneous +hz {{ redis_hz }} +aof-rewrite-incremental-fsync {{ redis_aof_rewrite_incremental_fsync }} + +# Security +{% if redis_requirepass %} +requirepass {{ redis_requirepass }} +{% endif %} + +{% for command in redis_disabled_commands %} +rename-command {{ command }} "" +{% endfor %} + +# Includes +{% for include in redis_includes %} +include {{ include }} +{% endfor %} + +# Custom configuration +{% for key, value in redis_extra_config.items() %} +{{ key }} {{ value }} +{% endfor %} \ No newline at end of file diff --git a/roles/rollback/defaults/main.yml b/roles/rollback/defaults/main.yml new file mode 100644 index 0000000000..08d321ee62 --- /dev/null +++ b/roles/rollback/defaults/main.yml @@ -0,0 +1,2 @@ +project_root: "{{ www_root }}/{{ site }}" +project_current_path: "{{ wordpress_sites[site].current_path | default('current') }}" diff --git a/roles/rollback/tasks/main.yml b/roles/rollback/tasks/main.yml index 4185f2729b..6ceb07dc30 100644 --- a/roles/rollback/tasks/main.yml +++ b/roles/rollback/tasks/main.yml @@ -1,22 +1,33 @@ --- +- name: Get real path of current symlinked release + command: "readlink {{ project_current_path }}" + args: + chdir: "{{ project_root }}" + register: current_release_readlink_result + changed_when: false + +- name: Clean up old and failed releases + deploy_helper: + state: clean + path: "{{ project_root }}" + current_path: "{{ project_current_path }}" + release: "{{ current_release_readlink_result.stdout }}" + keep_releases: "{{ project.deploy_keep_releases | default(deploy_keep_releases | default(omit)) }}" + - import_tasks: user-release.yml when: release is defined - import_tasks: prior-release.yml when: release is not defined -- name: Check whether target release was from a successful deploy - stat: - path: "{{ new_release_path }}/DEPLOY_UNFINISHED" - register: target - -- name: Fail if target release was from failed deploy - fail: - msg: "Cannot switch to release at {{ new_release_path }}. It is from an unfinished deploy. You may manually specify a different release using --extra-vars='release=12345678901234'." - when: target.stat.exists | default(False) - - name: Link 'current' directory to target release file: path: "{{ project_root }}/{{ project_current_path }}" src: "{{ new_release_path }}" state: link + +- name: Write unfinished file to old symlinked release + file: + path: "{{ current_release_readlink_result.stdout }}/DEPLOY_UNFINISHED" + state: touch + mode: '0644' diff --git a/roles/rollback/tasks/prior-release.yml b/roles/rollback/tasks/prior-release.yml index 1788aeebe8..25a51ca884 100644 --- a/roles/rollback/tasks/prior-release.yml +++ b/roles/rollback/tasks/prior-release.yml @@ -1,9 +1,13 @@ --- - name: Get list position of current symlinked release - shell: "ls releases | grep -n $(basename $(readlink {{ project_current_path }})) | cut -f1 -d:" + shell: | + set -eo pipefail + ls releases | grep -n $(basename $(readlink {{ project_current_path }})) | cut -f1 -d: args: chdir: "{{ project_root }}" + executable: /bin/bash register: current_release_position + changed_when: false - name: Fail if current release is the oldest available release fail: @@ -15,6 +19,7 @@ args: chdir: "{{ project_root }}" register: releases + changed_when: false - name: Create new_release_path variable set_fact: diff --git a/roles/rollback/tasks/user-release.yml b/roles/rollback/tasks/user-release.yml index 6e356e1bc0..d360cccda3 100644 --- a/roles/rollback/tasks/user-release.yml +++ b/roles/rollback/tasks/user-release.yml @@ -9,11 +9,12 @@ args: chdir: "{{ project_root }}" register: current_release + changed_when: false - name: Fail if user-specified release doesn't exist or is already active fail: msg: "Cannot switch to release {{ release }}. Either it does not exist or it is the active release." - when: specified.stat.isdir | default(False) == False or current_release.stdout_lines[0] == release + when: not (specified.stat.isdir | default(False)) or current_release.stdout_lines[0] == release - name: Create new_release_path variable set_fact: diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml index 205420642a..2936d98ffc 100644 --- a/roles/sshd/defaults/main.yml +++ b/roles/sshd/defaults/main.yml @@ -46,11 +46,9 @@ sshd_login_grace_time: 30 sshd_macs_default: - hmac-sha2-512-etm@openssh.com - hmac-sha2-256-etm@openssh.com - - hmac-ripemd160-etm@openssh.com - umac-128-etm@openssh.com - hmac-sha2-512 - hmac-sha2-256 - - hmac-ripemd160 sshd_macs_extra: [] @@ -90,3 +88,10 @@ ssh_send_env: [] ssh_strict_host_key_checking: ask ssh_use_roaming: false + +sshd_packages_default: + openssh-server: "{{ apt_security_package_state }}" + openssh-client: "{{ apt_security_package_state }}" + +sshd_packages_custom: {} +sshd_packages: "{{ sshd_packages_default | combine(sshd_packages_custom) }}" diff --git a/roles/sshd/tasks/main.yml b/roles/sshd/tasks/main.yml index 3fa0bcb6c8..8034fed796 100644 --- a/roles/sshd/tasks/main.yml +++ b/roles/sshd/tasks/main.yml @@ -1,20 +1,24 @@ --- - name: Ensure latest SSH server and client are installed apt: - pkg: "{{ item }}" - state: latest - update_cache: true + name: "{{ item.key }}" + state: "{{ item.value }}" cache_valid_time: "{{ apt_cache_valid_time }}" - with_items: - - openssh-server - - openssh-client + loop: "{{ sshd_packages | dict2items }}" + loop_control: + label: "{{ item.key }}" notify: restart ssh +- name: Create the /run/sshd directory + file: + path: /run/sshd + state: directory + - name: Create a secure sshd_config template: src: "{{ sshd_config }}" dest: /etc/ssh/sshd_config - mode: 0600 + mode: '0600' validate: '/usr/sbin/sshd -T -f %s' notify: restart ssh @@ -22,7 +26,7 @@ template: src: "{{ ssh_config }}" dest: /etc/ssh/ssh_config - mode: 0644 + mode: '0644' - name: Remove Diffie-Hellman moduli of size < 2000 lineinfile: diff --git a/roles/sshd/templates/ssh_config.j2 b/roles/sshd/templates/ssh_config.j2 index a21eee8c8f..03910f3e07 100644 --- a/roles/sshd/templates/ssh_config.j2 +++ b/roles/sshd/templates/ssh_config.j2 @@ -17,7 +17,9 @@ Host * KexAlgorithms {{ (ssh_kex_algorithms_default | default(sshd_kex_algorithms_default) + ssh_kex_algorithms_extra | default(sshd_kex_algorithms_extra)) | join(',') }} MACs {{ (ssh_macs_default | default(sshd_macs_default) + ssh_macs_extra | default(sshd_macs_extra)) | join(',') }} PasswordAuthentication {{ ssh_password_authentication | default(sshd_password_authentication) | ternary('yes', 'no') }} - SendEnv {{ ssh_send_env | join(' ') }} + {% if ssh_send_env | count > 0 %} + SendEnv {{ ssh_send_env | join(' ') }} + {% endif -%} StrictHostKeyChecking {{ ssh_strict_host_key_checking }} UseRoaming {{ ssh_use_roaming | ternary('yes','no') }} {% endblock %} diff --git a/roles/sshd/templates/sshd_config.j2 b/roles/sshd/templates/sshd_config.j2 index a1b961d7d4..d4371fd4b2 100644 --- a/roles/sshd/templates/sshd_config.j2 +++ b/roles/sshd/templates/sshd_config.j2 @@ -13,7 +13,9 @@ ListenAddress {{ address }} Protocol {{ sshd_protocol }} -AcceptEnv {{ sshd_accept_env | join(' ') }} +{% if sshd_accept_env | count > 0 %} + AcceptEnv {{ sshd_accept_env | join(' ') }} +{% endif -%} AllowAgentForwarding {{ sshd_allow_agent_forwarding | ternary('yes', 'no') }} AllowTcpForwarding {{ sshd_allow_tcp_forwarding is string | ternary(sshd_allow_tcp_forwarding, sshd_allow_tcp_forwarding | ternary('yes', 'no')) }} ChallengeResponseAuthentication {{ sshd_challenge_response_authentication | ternary('yes', 'no') }} diff --git a/roles/ssmtp/defaults/main.yml b/roles/ssmtp/defaults/main.yml index d373337628..c7310cf09e 100644 --- a/roles/ssmtp/defaults/main.yml +++ b/roles/ssmtp/defaults/main.yml @@ -1,4 +1,15 @@ +ssmtp_package: ssmtp ssmtp_auth_method: LOGIN ssmtp_from_override: 'Yes' ssmtp_start_tls: 'Yes' ssmtp_tls: 'Yes' +# ssmtp_revaliases: +# - user: root +# from: from@example.com +# smtp_server: "{{ mail_smtp_server }}" +# - user: "{{ admin_user }}" +# from: from@example.com +# smtp_server: "{{ mail_smtp_server }}" +# - user: "{{ web_user }}" +# from: from@example.com +# smtp_server: "{{ mail_smtp_server }}" diff --git a/roles/ssmtp/tasks/main.yml b/roles/ssmtp/tasks/main.yml index 140402f1cf..c0e78ba3ec 100644 --- a/roles/ssmtp/tasks/main.yml +++ b/roles/ssmtp/tasks/main.yml @@ -1,12 +1,18 @@ --- - name: Install ssmtp apt: - name: ssmtp - state: present - update_cache: true + name: "{{ ssmtp_package }}" + state: "{{ ssmtp_package_state | default(apt_package_state) }}" cache_valid_time: "{{ apt_cache_valid_time }}" - name: ssmtp configuration template: src: ssmtp.conf.j2 dest: /etc/ssmtp/ssmtp.conf + mode: '0644' + +- name: ssmtp revaliases configuration + template: + src: revaliases.j2 + dest: /etc/ssmtp/revaliases + mode: '0644' diff --git a/roles/ssmtp/templates/revaliases.j2 b/roles/ssmtp/templates/revaliases.j2 new file mode 100644 index 0000000000..2d66f62d04 --- /dev/null +++ b/roles/ssmtp/templates/revaliases.j2 @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +{% if ssmtp_revaliases is defined and ssmtp_revaliases is not none %} +{% for item in ssmtp_revaliases %} +{% if item.user is defined and item.user is not none and item.from is defined and item.from is not none and item.smtp_server is defined and item.smtp_server is not none %} +{{ item.user }}:{{ item.from }}:{{ item.smtp_server }} +{% endif %} +{% endfor %} +{% endif %} diff --git a/roles/users/tasks/main.yml b/roles/users/tasks/main.yml index e1487bc5c6..13af67c9f0 100644 --- a/roles/users/tasks/main.yml +++ b/roles/users/tasks/main.yml @@ -3,7 +3,7 @@ group: name: "{{ item }}" state: present - with_items: "{{ users | sum(attribute='groups', start=[]) | list | unique }}" + loop: "{{ users | sum(attribute='groups', start=[]) | list | unique }}" - name: Ensure sudo group has sudo privileges lineinfile: @@ -28,37 +28,71 @@ when: not sshd_permit_root_login tags: sshd +- name: Hash user passwords + command: + cmd: | + mkpasswd -m sha-512 --salt={{ (item.salt | default(''))[:16] | regex_replace("[^\.\/a-zA-Z0-9]", "x") }} + stdin: "{{ item.password }}" + loop: "{{ vault_users | default([]) }}" + loop_control: + label: "{{ item.name }}" + when: item.password is defined + register: user_passwords_hashed + no_log: true + changed_when: false + - name: Setup users user: name: "{{ item.name }}" group: "{{ item.groups[0] }}" groups: "{{ item.groups | join(',') }}" - password: '{% for user in vault_users | default([]) if user.name == item.name and user.password is defined %}{{ user.password | password_hash("sha512", (user.salt | default(""))[:16] | regex_replace("[^\.\/a-zA-Z0-9]", "x")) }}{% else %}{{ None }}{% endfor %}' + password: "{{ (user_passwords_hashed.results | selectattr('item.name', 'equalto', item.name) | map(attribute='stdout') | first) | default(omit) }}" state: present shell: /bin/bash - update_password: always - with_items: "{{ users }}" + update_password: "{{ item.update_password | default('always') }}" + loop: "{{ users }}" + loop_control: + label: "{{ item.name }}" - name: Add web user sudoers items for services template: src: sudoers.d.j2 dest: "/etc/sudoers.d/{{ web_user }}-services" - mode: 0440 + mode: '0440' owner: root group: root validate: "/usr/sbin/visudo -cf %s" - when: web_sudoers + when: web_sudoers[0] is defined + +- name: Replace all user SSH keys with first non-empty key + authorized_key: + user: "{{ item.name }}" + key: "{{ (item['keys'] | select('truthy') | list).0 }}" + exclusive: true + loop: "{{ users | default([]) }}" + loop_control: + label: "{{ item.name }}" + when: + - reset_user_ssh_keys | default(false) + - (item['keys'] | select('truthy') | list | length) > 0 -- name: Add SSH keys +- name: Add user SSH keys authorized_key: user: "{{ item.0.name }}" key: "{{ item.1 }}" - with_subelements: - - "{{ users | default([]) }}" - - keys + loop: "{{ users | default([]) | subelements('keys') }}" + loop_control: + label: "{{ item.0.name }}" + +- name: Add deploy SSH keys + authorized_key: + user: "{{ web_user }}" + key: "{{ lookup('file', item) }}" + with_fileglob: 'public_keys/*.pub' - name: Check whether Ansible can connect as admin_user - local_action: command ansible {{ inventory_hostname }} -m ping -u {{ admin_user }} {{ cli_options | default('') }} + command: ansible {{ inventory_hostname }} -m ping -u {{ admin_user }} {{ cli_options | default('') }} + delegate_to: localhost failed_when: false changed_when: false become: no @@ -67,5 +101,5 @@ tags: [connection-tests, sshd] - import_tasks: connection-warnings.yml - when: not admin_user_status | skipped and admin_user_status.rc != 0 + when: admin_user_status is not skipped and admin_user_status.rc != 0 tags: [connection-tests, sshd] diff --git a/roles/wordpress-install/tasks/composer-authentications.yml b/roles/wordpress-install/tasks/composer-authentications.yml new file mode 100644 index 0000000000..217fe2a248 --- /dev/null +++ b/roles/wordpress-install/tasks/composer-authentications.yml @@ -0,0 +1,46 @@ +--- +- name: "Setup composer authentications (HTTP Basic) - {{ site }}" + composer: + command: config + arguments: --auth http-basic.{{ item.hostname | quote }} {{ item.username | quote }} {{ item.password | default("") | quote }} + working_dir: "{{ working_dir }}" + become: no + no_log: true + changed_when: false + when: + - item.hostname is defined and item.hostname != "" + - item.username is defined and item.username != "" + loop: "{{ composer_authentications_using_basic_auth }}" + loop_control: + label: "{{ item.type | default('default-type') }}.{{ item.hostname }}" + +- name: "Setup composer authentications (BitBucket OAuth) - {{ site }}" + composer: + command: config + arguments: --auth bitbucket-oauth.{{ item.hostname | quote }} {{ item.consumer_key | quote }} {{ item.consumer_secret | quote }} + working_dir: "{{ working_dir }}" + become: no + no_log: true + changed_when: false + when: + - item.hostname is defined and item.hostname != "" + - item.consumer_key is defined and item.consumer_key != "" + - item.consumer_secret is defined and item.consumer_secret != "" + loop: "{{ composer_authentications_using_bitbucket_oauth }}" + loop_control: + label: "{{ item.type }}.{{ item.hostname }}" + +- name: "Setup composer authentications (Other Tokens) - {{ site }}" + composer: + command: config + arguments: --auth {{ item.type | quote }}.{{ item.hostname | quote }} {{ item.token | quote }} + working_dir: "{{ working_dir }}" + become: no + no_log: true + changed_when: false + when: + - item.hostname is defined and item.hostname != "" + - item.token is defined and item.token != "" + loop: "{{ composer_authentications_using_other_token }}" + loop_control: + label: "{{ item.type }}.{{ item.hostname }}" diff --git a/roles/wordpress-install/tasks/directories.yml b/roles/wordpress-install/tasks/directories.yml index 093d3212a6..ed9277812f 100644 --- a/roles/wordpress-install/tasks/directories.yml +++ b/roles/wordpress-install/tasks/directories.yml @@ -1,21 +1,27 @@ --- - name: Create web root of sites file: - path: "{{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/web" + path: "{{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/{{ item.value.public_path | default('web') }}" owner: "{{ web_user }}" group: "{{ web_group }}" - mode: 0755 + mode: '0755' state: directory - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + become_method: sudo - name: Create shared folder of sites file: path: "{{ www_root }}/{{ item.key }}/shared" owner: "{{ web_user }}" group: "{{ web_group }}" - mode: 0755 + mode: '0755' state: directory - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + become_method: sudo - name: Change site owner to user file: @@ -24,4 +30,8 @@ group: "{{ web_group }}" state: directory recurse: yes - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + become_method: sudo + when: chown_site_directory | default(false) diff --git a/roles/wordpress-install/tasks/dotenv.yml b/roles/wordpress-install/tasks/dotenv.yml new file mode 100644 index 0000000000..af744be89d --- /dev/null +++ b/roles/wordpress-install/tasks/dotenv.yml @@ -0,0 +1,21 @@ +--- +- name: Create .env file + template: + src: "env.j2" + dest: "/tmp/{{ item.key }}.env" + mode: '0644' + owner: "{{ web_user }}" + group: "{{ web_group }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + +- name: Copy .env file into web root + synchronize: + src: "/tmp/{{ item.key }}.env" + dest: "{{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/.env" + checksum: true + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + delegate_to: "{{ inventory_hostname }}" diff --git a/roles/wordpress-install/tasks/main.yml b/roles/wordpress-install/tasks/main.yml index 27e3458985..9f1f7beaec 100644 --- a/roles/wordpress-install/tasks/main.yml +++ b/roles/wordpress-install/tasks/main.yml @@ -2,19 +2,8 @@ - import_tasks: directories.yml tags: wordpress-install-directories -- name: Create .env file - template: - src: "env.j2" - dest: "/tmp/{{ item.key }}.env" - owner: "{{ web_user }}" - group: "{{ web_group }}" - with_dict: "{{ wordpress_sites }}" - -- name: Copy .env file into web root - command: rsync -ac --info=NAME /tmp/{{ item.key }}.env {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/.env - with_dict: "{{ wordpress_sites }}" - register: env_file - changed_when: env_file.stdout == item.key + '.env' +- import_tasks: dotenv.yml + tags: dotenv - name: Add known_hosts known_hosts: @@ -23,16 +12,19 @@ path: "{{ item.path | default(omit) }}" state: "{{ item.state | default('present') }}" become: no - with_items: "{{ known_hosts | default([]) }}" + loop: "{{ known_hosts | default([]) }}" + loop_control: + label: "{{ item.name }}" -- name: Setup packagist.com authentication - composer: - command: config - arguments: --auth http-basic.repo.packagist.com token {{ item.value.packagist_token }} - working_dir: "{{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/" +- include_tasks: tasks/composer-authentications.yml + vars: + site: "{{ loop_item.key }}" + working_dir: "{{ www_root }}/{{ loop_item.key }}/{{ loop_item.value.current_path | default('current') }}/" no_log: true - when: item.value.packagist_token is defined - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + loop_var: loop_item # cannot be 'item' or 'site'. + label: "{{ loop_item.key }}" - name: Install Dependencies with Composer composer: @@ -40,7 +32,9 @@ optimize_autoloader: no working_dir: "{{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/" become: no - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Install WP command: wp core {{ item.value.multisite.enabled | default(false) | ternary('multisite-install', 'install') }} @@ -48,16 +42,21 @@ --url="{{ site_env.wp_home }}" {% if item.value.multisite.enabled | default(false) %} --base="{{ item.value.multisite.base_path | default('/') }}" - --subdomains="{{ item.value.multisite.subdomains | default('false') }}" + {% if item.value.multisite.subdomains | default('false') %} + --subdomains + {% endif %} {% endif %} --title="{{ item.value.site_title | default(item.key) }}" --admin_user="{{ item.value.admin_user | default('admin') }}" --admin_password="{{ vault_wordpress_sites[item.key].admin_password }}" --admin_email="{{ item.value.admin_email }}" + become: no args: chdir: "{{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/" register: wp_install - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" when: item.value.site_install | default(true) changed_when: "'WordPress is already installed.' not in wp_install.stdout and 'The network already exists.' not in wp_install.stdout" @@ -65,12 +64,16 @@ command: wp rewrite structure {{ item.item.value.initial_permalink_structure | default("/%postname%/") }} --allow-root args: chdir: "{{ www_root }}/{{ item.item.key }}/{{ item.item.value.current_path | default('current') }}/" - with_items: "{{ wp_install.results }}" - when: item | changed + loop: "{{ wp_install.results }}" + loop_control: + label: "{{ item.item.key }}" + when: item is changed - name: Update WP Multisite Home URL command: wp option update home {{ site_env.wp_home }} --allow-root args: chdir: "{{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/" - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" when: item.value.site_install | default(true) and item.value.multisite.enabled | default(false) diff --git a/roles/wordpress-setup/defaults/main.yml b/roles/wordpress-setup/defaults/main.yml index b34865ca89..87197b6df5 100644 --- a/roles/wordpress-setup/defaults/main.yml +++ b/roles/wordpress-setup/defaults/main.yml @@ -2,25 +2,53 @@ site_uses_local_db: "{{ site_env.db_host == 'localhost' }}" nginx_wordpress_site_conf: wordpress-site.conf.j2 nginx_ssl_path: "{{ nginx_path }}/ssl" +nginx_sites_confs: + - src: no-default.conf.j2 + - src: ssl.no-default.conf.j2 + enabled: false + +nginx_http2_enabled: true +nginx_http3_enabled: false + # HSTS defaults nginx_hsts_max_age: 31536000 -nginx_hsts_include_subdomains: true +nginx_hsts_include_subdomains: false nginx_hsts_preload: false # HSTS helpers hsts_max_age: "{{ item.value.ssl.hsts_max_age | default(nginx_hsts_max_age) }}" -hsts_include_subdomains: "{{ item.value.ssl.hsts_include_subdomains | default(nginx_hsts_include_subdomains) | ternary('includeSubDomains', None) }}" -hsts_preload: "{{ item.value.ssl.hsts_preload | default(nginx_hsts_preload) | ternary('preload', None) }}" +hsts_include_subdomains: "{{ item.value.ssl.hsts_include_subdomains | default(nginx_hsts_include_subdomains) | ternary('includeSubDomains', omit) }}" +hsts_preload: "{{ item.value.ssl.hsts_preload | default(nginx_hsts_preload) | ternary('preload', omit) }}" # Fastcgi cache params nginx_cache_duration: 30s nginx_skip_cache_uri: /wp-admin/|/wp-json/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml nginx_skip_cache_cookie: comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in +nginx_cache_background_update: "on" + +# Cache configuration helpers +cache_config: "{{ item.value.cache | default({}) }}" +object_cache_config: "{{ item.value.object_cache | default({}) }}" + +# FastCGI cache - enabled when cache.enabled is true (backward compatible) +fastcgi_cache_enabled: "{{ cache_config.enabled | default(false) }}" + +# Object cache - enabled when object_cache.enabled is true +object_cache_enabled: "{{ object_cache_config.enabled | default(false) }}" +object_cache_provider: "{{ object_cache_config.provider | default('redis') }}" + +# Object cache defaults +redis_host: "{{ object_cache_config.host | default('127.0.0.1') }}" +redis_port: "{{ object_cache_config.port | default(6379) }}" +redis_database: "{{ object_cache_config.database | default(0) }}" +redis_password: "{{ object_cache_config.password | default('') }}" +redis_prefix: "{{ object_cache_config.prefix | default('') }}" +memcached_host: "{{ object_cache_config.host | default('127.0.0.1') }}" +memcached_port: "{{ object_cache_config.port | default(11211) }}" # Nginx includes nginx_includes_templates_path: nginx-includes -nginx_includes_deprecated: roles/wordpress-setup/templates/includes.d -nginx_includes_pattern: "^({{ nginx_includes_templates_path | regex_escape }}|{{ nginx_includes_deprecated | regex_escape }})/(.*)\\.j2$" +nginx_includes_pattern: "^({{ nginx_includes_templates_path | regex_escape }})/(.*)\\.j2$" nginx_includes_d_cleanup: true # h5bp helpers @@ -35,7 +63,13 @@ h5bp_cross_domain_fonts_enabled: "{{ h5bp.cross_domain_fonts | default(true) }}" h5bp_expires_enabled: "{{ h5bp.expires | default(false) }}" h5bp_protect_system_files_enabled: "{{ h5bp.protect_system_files | default(true) }}" +# X-Robots-Tag Header helpers +not_prod: "{{ env != 'production' }}" +robots_tag_header: "{{ item.value.robots_tag_header | default({}) }}" +robots_tag_header_enabled: "{{ robots_tag_header.enabled | default(not_prod) }}" + # PHP FPM +php_fpm_pm: 'dynamic' php_fpm_pm_max_children: 10 php_fpm_pm_start_servers: 1 php_fpm_pm_min_spare_servers: 1 diff --git a/roles/wordpress-setup/tasks/database.yml b/roles/wordpress-setup/tasks/database.yml index bde2f14f30..f238b977a3 100644 --- a/roles/wordpress-setup/tasks/database.yml +++ b/roles/wordpress-setup/tasks/database.yml @@ -1,13 +1,16 @@ --- - block: - - name: Create database of sites + - name: Create databases for sites mysql_db: name: "{{ site_env.db_name }}" state: present login_host: "{{ site_env.db_host }}" login_user: "{{ mysql_root_user }}" login_password: "{{ mysql_root_password }}" - with_dict: "{{ wordpress_sites }}" + no_log: true + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Create/assign database user to db and grant permissions mysql_user: @@ -20,6 +23,10 @@ login_host: "{{ site_env.db_host }}" login_user: "{{ mysql_root_user }}" login_password: "{{ mysql_root_password }}" - with_dict: "{{ wordpress_sites }}" + column_case_sensitive: no + no_log: true + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" when: site_uses_local_db and item.value.db_create | default(true) diff --git a/roles/wordpress-setup/tasks/main.yml b/roles/wordpress-setup/tasks/main.yml index 732d95e383..890d7078aa 100644 --- a/roles/wordpress-setup/tasks/main.yml +++ b/roles/wordpress-setup/tasks/main.yml @@ -11,7 +11,7 @@ path: "{{ www_root }}" owner: "{{ web_user }}" group: "{{ web_group }}" - mode: 0755 + mode: '0755' state: directory - name: Create logs folder of sites @@ -19,20 +19,23 @@ path: "{{ www_root }}/{{ item.key }}/logs" owner: "{{ web_user }}" group: "{{ web_group }}" - mode: 0755 + mode: '0755' state: directory - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Create WordPress php-fpm configuration file template: - src: php-fpm.conf.j2 - dest: /etc/php/7.1/fpm/pool.d/wordpress.conf + src: php-fpm-pool-wordpress.conf.j2 + dest: /etc/php/{{ php_version }}/fpm/pool.d/wordpress.conf + mode: '0644' notify: reload php-fpm - name: Disable default PHP-FPM pool - command: mv /etc/php/7.1/fpm/pool.d/www.conf /etc/php/7.1/fpm/pool.d/www.disabled + command: mv /etc/php/{{ php_version }}/fpm/pool.d/www.conf /etc/php/{{ php_version }}/fpm/pool.d/www.disabled args: - creates: /etc/php/7.1/fpm/pool.d/www.disabled + creates: /etc/php/{{ php_version }}/fpm/pool.d/www.disabled when: disable_default_pool | default(true) notify: reload php-fpm @@ -45,19 +48,23 @@ - name: Setup WP system cron cron: name: "{{ item.key }} WordPress cron" - minute: "*/15" + minute: "{{ item.value.cron_interval | default('*/15') }}" user: "{{ web_user }}" job: "cd {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }} && wp cron event run --due-now > /dev/null 2>&1" cron_file: "wordpress-{{ item.key | replace('.', '_') }}" - with_dict: "{{ wordpress_sites }}" - when: cron_enabled and not item.value.multisite.enabled + state: "{{ (cron_enabled and not item.value.multisite.enabled) | ternary('present', 'absent') }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" - name: Setup WP Multisite system cron cron: name: "{{ item.key }} WordPress network cron" - minute: "*/30" + minute: "{{ item.value.cron_interval_multisite | default('*/30') }}" user: "{{ web_user }}" - job: "cd {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }} && wp site list --field=url | xargs -n1 -I \\% wp --url=\\% cron event run --due-now > /dev/null 2>&1" + job: "cd {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }} && (wp site list --field=url | xargs -n1 -I \\% wp --url=\\% cron event run --due-now) > /dev/null 2>&1" cron_file: "wordpress-multisite-{{ item.key | replace('.', '_') }}" - with_dict: "{{ wordpress_sites }}" - when: cron_enabled and item.value.multisite.enabled + state: "{{ (cron_enabled and item.value.multisite.enabled) | ternary('present', 'absent') }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" diff --git a/roles/wordpress-setup/tasks/nginx-client-cert.yml b/roles/wordpress-setup/tasks/nginx-client-cert.yml index 49d810eee5..c6f7a6e03f 100644 --- a/roles/wordpress-setup/tasks/nginx-client-cert.yml +++ b/roles/wordpress-setup/tasks/nginx-client-cert.yml @@ -3,6 +3,8 @@ get_url: url: "{{ item.value.ssl.client_cert_url }}" dest: "{{ nginx_ssl_path }}/client-{{ (item.value.ssl.client_cert_url | hash('md5'))[:7] }}.crt" - mode: 0640 - with_dict: "{{ wordpress_sites }}" - when: ssl_enabled and item.value.ssl.client_cert_url is defined + mode: '0640' + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: ssl_enabled and 'client_cert_url' in item.value.ssl diff --git a/roles/wordpress-setup/tasks/nginx-includes.yml b/roles/wordpress-setup/tasks/nginx-includes.yml index a9859b0afb..703915c720 100644 --- a/roles/wordpress-setup/tasks/nginx-includes.yml +++ b/roles/wordpress-setup/tasks/nginx-includes.yml @@ -1,54 +1,70 @@ --- -- name: Build list of Nginx includes templates - find: - paths: - - "{{ nginx_includes_templates_path }}" - - "{{ nginx_includes_deprecated }}" - pattern: "*.conf.j2" - recurse: yes +- stat: + path: "{{ nginx_includes_templates_path }}" become: no - connection: local - register: nginx_includes_templates + delegate_to: localhost + register: nginx_includes_local_directory -- name: Warn about deprecated Nginx includes directory - debug: - msg: "[DEPRECATION WARNING]: The `{{ nginx_includes_deprecated }}` directory for Trellis Nginx includes templates is deprecated and will no longer function beginning with Trellis 1.0. Please move these templates to a directory named `{{ nginx_includes_templates_path }}` in the root of this project. For more information, see https://roots.io/trellis/docs/nginx-includes/" - when: True in nginx_includes_templates.files | map(attribute='path') | map('search', nginx_includes_deprecated | regex_escape) | list +- block: + - name: Loop through site keys to build a list of Nginx includes templates per folder + find: + paths: "{{ nginx_includes_templates_path }}/{{ item }}" + patterns: "*.conf.j2" + recurse: yes + loop: "{{ wordpress_sites.keys() | list + ['all'] }}" + become: no + delegate_to: localhost + register: nginx_includes_folder_results -- name: Create includes.d directories - file: - path: "{{ nginx_path }}/includes.d/{{ item }}" - state: directory - mode: 0755 - with_items: "{{ nginx_includes_templates.files | map(attribute='path') | - map('regex_replace', nginx_includes_pattern, '\\2') | - map('dirname') | unique | list | sort - }}" - when: nginx_includes_templates.files | count + - name: Flatten include template files into one list + set_fact: + nginx_includes_templates: + files: "{{ nginx_includes_folder_results.results | map(attribute='files') | flatten }}" -- name: Template files out to includes.d - template: - src: "{{ item }}" - dest: "{{ nginx_path }}/includes.d/{{ item | regex_replace(nginx_includes_pattern, '\\2') }}" - with_items: "{{ nginx_includes_templates.files | map(attribute='path') | list | sort(True) }}" - notify: reload nginx + - name: Create includes.d directories + file: + path: "{{ nginx_path }}/includes.d/{{ item }}" + state: directory + recurse: yes + mode: '0755' + loop: "{{ nginx_includes_templates.files | map(attribute='path') | + map('regex_replace', nginx_includes_pattern, '\\2') | + map('dirname') | unique | list | sort + }}" + when: nginx_includes_templates.files | count > 0 -- name: Retrieve list of existing files in includes.d - find: - paths: "{{ nginx_path }}/includes.d" - pattern: "*.conf" - recurse: yes - register: nginx_includes_existing - when: nginx_includes_d_cleanup + - name: Template files out to includes.d + template: + src: "{{ item }}" + dest: "{{ nginx_path }}/includes.d/{{ item | regex_replace(nginx_includes_pattern, '\\2') }}" + mode: '0644' + loop: "{{ nginx_includes_templates.files | map(attribute='path') | list | sort(True) }}" + notify: reload nginx + when: nginx_includes_local_directory.stat.isdir is defined -- name: Remove unmanaged files from includes.d - file: - path: "{{ item }}" - state: absent - with_items: "{{ nginx_includes_existing.files | default({}) | map(attribute='path') | - difference(nginx_includes_templates.files | map(attribute='path') | - map('regex_replace', nginx_includes_pattern, nginx_path + '/includes.d/\\2') | unique - ) | list - }}" - when: nginx_includes_d_cleanup - notify: reload nginx +- name: Cleanup old unmanaged Nginx includes + block: + - stat: + path: "{{ nginx_path }}/includes.d" + register: nginx_includes_directory + + - name: Retrieve list of existing files in includes.d + find: + paths: "{{ nginx_path }}/includes.d" + pattern: "*.conf" + recurse: yes + register: nginx_includes_existing + when: nginx_includes_directory.stat.isdir is defined + + - name: Remove unmanaged files from includes.d + file: + path: "{{ item }}" + state: absent + loop: "{{ nginx_includes_existing.files | default({}) | map(attribute='path') | + difference(nginx_includes_templates.files | default({} )| map(attribute='path') | + map('regex_replace', nginx_includes_pattern, nginx_path + '/includes.d/\\2') | unique + ) | list + }}" + when: nginx_includes_directory.stat.isdir is defined + notify: reload nginx + when: nginx_includes_d_cleanup | bool diff --git a/roles/wordpress-setup/tasks/nginx.yml b/roles/wordpress-setup/tasks/nginx.yml index e2d9b58cc8..183bf9b91f 100644 --- a/roles/wordpress-setup/tasks/nginx.yml +++ b/roles/wordpress-setup/tasks/nginx.yml @@ -3,33 +3,71 @@ copy: src: "{{ item.value.ssl.cert }}" dest: "{{ nginx_ssl_path }}/{{ item.value.ssl.cert | basename }}" - mode: 0640 - with_dict: "{{ wordpress_sites }}" - when: ssl_enabled and item.value.ssl.cert is defined + mode: '0640' + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: ssl_enabled and 'cert' in item.value.ssl notify: reload nginx - name: Copy SSL key copy: src: "{{ item.value.ssl.key }}" dest: "{{ nginx_ssl_path }}/{{ item.value.ssl.key | basename }}" - mode: 0600 - with_dict: "{{ wordpress_sites }}" - when: ssl_enabled and item.value.ssl.key is defined + mode: '0600' + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: ssl_enabled and 'key' in item.value.ssl notify: reload nginx - import_tasks: "{{ playbook_dir }}/roles/common/tasks/disable_challenge_sites.yml" +- name: Create Nginx available sites + template: + src: "{{ item.src }}" + dest: "{{ nginx_path }}/sites-available/{{ item.src | basename | regex_replace('.j2$', '') }}" + mode: '0644' + loop: "{{ nginx_sites_confs }}" + when: item.enabled | default(true) + notify: reload nginx + tags: nginx-sites + +- name: Disable Nginx sites + file: + path: "{{ nginx_path }}/sites-enabled/{{ item.src | basename | regex_replace('.j2$', '') }}" + state: absent + when: not(item.enabled | default(true)) + loop: "{{ nginx_sites_confs }}" + notify: reload nginx + tags: nginx-sites + +- name: Enable Nginx sites + file: + path: "{{ nginx_path }}/sites-enabled/{{ item.src | basename | regex_replace('.j2$', '') }}" + src: "{{ nginx_path }}/sites-available/{{ item.src | basename | regex_replace('.j2$', '') }}" + state: link + force: yes + loop: "{{ nginx_sites_confs }}" + when: item.enabled | default(true) + notify: reload nginx + tags: nginx-sites + - name: Create Nginx conf for challenges location template: src: "{{ playbook_dir }}/roles/letsencrypt/templates/acme-challenge-location.conf.j2" dest: "{{ nginx_path }}/acme-challenge-location.conf" + mode: '0644' notify: reload nginx - name: Create WordPress configuration for Nginx template: src: "{{ item.value.nginx_wordpress_site_conf | default(nginx_wordpress_site_conf) }}" dest: "{{ nginx_path }}/sites-available/{{ item.key }}.conf" - with_dict: "{{ wordpress_sites }}" + mode: '0644' + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" notify: reload nginx tags: nginx-includes @@ -40,5 +78,7 @@ owner: root group: root state: link - with_dict: "{{ wordpress_sites }}" + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" notify: reload nginx diff --git a/roles/wordpress-setup/tasks/self-signed-certificate.yml b/roles/wordpress-setup/tasks/self-signed-certificate.yml index 80c6600cb1..7b1fe2dd55 100644 --- a/roles/wordpress-setup/tasks/self-signed-certificate.yml +++ b/roles/wordpress-setup/tasks/self-signed-certificate.yml @@ -1,23 +1,41 @@ --- +- name: Ensure openssl configs directory are present + file: + path: "{{ nginx_ssl_path }}/self-signed-openssl-configs/" + state: directory + mode: '0755' + +- name: Template openssl configs + template: + src: self-signed-openssl-config.j2 + dest: "{{ nginx_ssl_path }}/self-signed-openssl-configs/{{ item.key }}.cnf" + mode: '0644' + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: + - sites_use_ssl | bool + - ssl_enabled | bool + - item.value.ssl.provider | default('manual') == 'self-signed' + - name: Generate self-signed certificates - shell: "openssl req -new -newkey rsa:2048 \ - -days 3650 -nodes -x509 -sha256 \ - -extensions req_ext -config <( \ -cat <<' EOF'\n -[req]\n -prompt = no\n -distinguished_name = req_dn\n -[req_dn]\n -commonName = {{ item.value.site_hosts[0].canonical }}\n -[req_ext]\n -subjectAltName = {{ site_hosts | union(multisite_subdomains_wildcards) | map('regex_replace', '(.*)', 'DNS:\\1') | join(',') }}\n -EOF\n - ) \ + command: "openssl req -new -newkey rsa:2048 \ + -days 825 -nodes -x509 -sha256 \ + -extensions req_ext -config {{ nginx_ssl_path }}/self-signed-openssl-configs/{{ item.key }}.cnf \ -keyout {{ item.key | quote }}.key -out {{ item.key | quote }}.cert" args: - executable: "/bin/bash" chdir: "{{ nginx_ssl_path }}" creates: "{{ item.key }}.*" - with_dict: "{{ wordpress_sites }}" - when: ssl_enabled and item.value.ssl.provider | default('manual') == 'self-signed' + loop: "{{ wordpress_sites | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: + - sites_use_ssl | bool + - ssl_enabled | bool + - item.value.ssl.provider | default('manual') == 'self-signed' notify: reload nginx + +- name: Clean up openssl configs directory + file: + path: "{{ nginx_ssl_path }}/self-signed-openssl-configs/" + state: absent diff --git a/roles/nginx/templates/no-default.conf.j2 b/roles/wordpress-setup/templates/no-default.conf.j2 similarity index 67% rename from roles/nginx/templates/no-default.conf.j2 rename to roles/wordpress-setup/templates/no-default.conf.j2 index 3a9bff2df6..6a047dee7b 100644 --- a/roles/nginx/templates/no-default.conf.j2 +++ b/roles/wordpress-setup/templates/no-default.conf.j2 @@ -12,3 +12,13 @@ server { listen 80 default_server deferred; return 444; } + +{% if sites_use_ssl %} +server { + listen [::]:443 ssl default_server deferred; + listen 443 ssl default_server deferred; + + ssl_reject_handshake on; + include h5bp/directive-only/ssl.conf; +} +{% endif %} diff --git a/roles/wordpress-setup/templates/php-fpm.conf.j2 b/roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2 similarity index 97% rename from roles/wordpress-setup/templates/php-fpm.conf.j2 rename to roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2 index 1464745633..46699c9291 100644 --- a/roles/wordpress-setup/templates/php-fpm.conf.j2 +++ b/roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2 @@ -6,7 +6,7 @@ listen.owner = www-data listen.group = www-data user = {{ web_user }} group = {{ web_group }} -pm = dynamic +pm = {{ php_fpm_pm }} pm.max_children = {{ php_fpm_pm_max_children }} pm.start_servers = {{ php_fpm_pm_start_servers }} pm.min_spare_servers = {{ php_fpm_pm_min_spare_servers }} diff --git a/roles/wordpress-setup/templates/self-signed-openssl-config.j2 b/roles/wordpress-setup/templates/self-signed-openssl-config.j2 new file mode 100644 index 0000000000..9ba1054aef --- /dev/null +++ b/roles/wordpress-setup/templates/self-signed-openssl-config.j2 @@ -0,0 +1,7 @@ +[req] +prompt = no +distinguished_name = req_dn +[req_dn] +commonName = {{ item.value.site_hosts[0].canonical }} +[req_ext] +subjectAltName = {{ site_hosts | union(multisite_subdomains_wildcards) | map('regex_replace', '(^.*$)', 'DNS:\\1') | join(',') }} diff --git a/roles/wordpress-setup/templates/wordpress-site.conf.j2 b/roles/wordpress-setup/templates/wordpress-site.conf.j2 index 3b8905846e..d1d67232d8 100644 --- a/roles/wordpress-setup/templates/wordpress-site.conf.j2 +++ b/roles/wordpress-setup/templates/wordpress-site.conf.j2 @@ -4,8 +4,10 @@ server { {% block server_id -%} - listen {{ ssl_enabled | ternary('[::]:443 ssl http2', '[::]:80') }}; - listen {{ ssl_enabled | ternary('443 ssl http2', '80') }}; + listen {{ ssl_enabled | ternary('[::]:443 ssl', '[::]:80') }}; + listen {{ ssl_enabled | ternary('443 ssl', '80') }}; + http2 {{ nginx_http2_enabled | default(false) | ternary('on', 'off') }}; + http3 {{ nginx_http3_enabled | default(false) | ternary('on', 'off') }}; server_name {{ site_hosts_canonical | union(multisite_subdomains_wildcards) | join(' ') }}; {% endblock %} @@ -15,10 +17,13 @@ server { {% endblock %} {% block server_basic -%} - root {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/web; + root {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/{{ item.value.public_path | default('web') }}; index index.php index.htm index.html; add_header Fastcgi-Cache $upstream_cache_status; + # Additional proxy header for WordPress (notably its Site Health check) + add_header X-Cache-Enabled $loopback_header_x_cache_enabled; + # Specify a charset charset utf-8; @@ -33,9 +38,15 @@ server { {% endblock -%} {% block cache_conditions -%} - {% if item.value.cache is defined and item.value.cache.enabled | default(false) -%} + {% if fastcgi_cache_enabled -%} # Fastcgi cache conditions set $skip_cache 0; + + # Skip requests with HTTP methods that should not be cached: DELETE, OPTIONS, PATCH, POST, PUT + if ($request_method !~ ^(GET|HEAD)$) { + set $skip_cache 1; + } + if ($query_string != "") { set $skip_cache 1; } @@ -71,26 +82,24 @@ server { {% if ssl_enabled -%} # SSL configuration include h5bp/directive-only/ssl.conf; - {% if ssl_stapling_enabled -%} - include h5bp/directive-only/ssl-stapling.conf; - {% endif -%} - ssl_dhparam /etc/nginx/ssl/dhparams.pem; ssl_buffer_size 1400; # 1400 bytes to fit in one MTU - add_header Strict-Transport-Security "max-age={{ [hsts_max_age, hsts_include_subdomains, hsts_preload] | reject('none') | join('; ') }}"; + {% if item.value.ssl.provider | default('manual') != 'self-signed' -%} + add_header Strict-Transport-Security "max-age={{ [hsts_max_age, hsts_include_subdomains, hsts_preload] | reject('equalto', omit) | join('; ') | trim }}"; + {% endif -%} - {% if item.value.ssl.client_cert_url is defined -%} + {% if 'client_cert_url' in item.value.ssl -%} ssl_verify_client on; ssl_client_certificate {{ nginx_ssl_path }}/client-{{ (item.value.ssl.client_cert_url | hash('md5'))[:7] }}.crt; {% endif -%} - {% if item.value.ssl.provider | default('manual') == 'manual' and item.value.ssl.cert is defined and item.value.ssl.key is defined -%} + {% if item.value.ssl.provider | default('manual') == 'manual' and 'cert' in item.value.ssl and 'key' in item.value.ssl -%} ssl_certificate {{ nginx_path }}/ssl/{{ item.value.ssl.cert | basename }}; ssl_certificate_key {{ nginx_path }}/ssl/{{ item.value.ssl.key | basename }}; {% elif item.value.ssl.provider | default('manual') == 'letsencrypt' -%} - ssl_certificate {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}-bundled.cert; + ssl_certificate {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}-bundled.cert; ssl_certificate_key {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}.key; {% elif item.value.ssl.provider | default('manual') == 'self-signed' -%} @@ -108,13 +117,62 @@ server { {% endblock -%} {% block includes_d -%} + include includes.d/all/*.conf; include includes.d/{{ item.key }}/*.conf; {% endblock -%} {% block location_uploads_php -%} # Prevent PHP scripts from being executed inside the uploads folder. - location ~* /app/uploads/.*\.php$ { + location ~* /{{ item.value.upload_path | default('app/uploads') }}/.*\.php$ { + deny all; + } + {% endblock %} + + {% block blade_twig_templates -%} + # Prevent Blade and Twig templates from being accessed directly. + location ~* \.(blade\.php|twig)$ { + deny all; + } + {% endblock %} + + {% block dependency_managers -%} + # composer + location ~* composer\.(json|lock)$ { + deny all; + } + + location ~* composer/installed\.json$ { + deny all; + } + + location ~* auth\.json$ { + deny all; + } + + # npm + location ~* package(-lock)?\.json$ { + deny all; + } + + # yarn + location ~* yarn\.lock$ { + deny all; + } + + # bundler + location ~* Gemfile(\.lock)?$ { + deny all; + } + + location ~* gems\.(rb|locked)?$ { + deny all; + } + {% endblock %} + + {% block plugin_theme_docs_files -%} + # Block .txt and .md files in plugins, mu-plugins, and themes directories to prevent version disclosure + location ~* /app/(plugins|mu-plugins|themes)/.+\.(txt|md)$ { deny all; } {% endblock %} @@ -125,6 +183,14 @@ server { } {% endblock %} + {% block disable_xmlrpc -%} + {% if 'xmlrpc' in item.value and item.value.xmlrpc.enabled | default(true) == false %} + location ~* xmlrpc\.php$ { + return 444; + } + {% endif %} + {% endblock %} + {% block h5bp -%} {% if h5bp_cache_file_descriptors_enabled -%} include h5bp/directive-only/cache-file-descriptors.conf; @@ -147,11 +213,11 @@ server { {% endif -%} {% if h5bp_cross_domain_fonts_enabled -%} - include h5bp/location/cross-domain-fonts.conf; + include h5bp/directive-only/cross-origin-requests.conf; {% endif -%} {% if h5bp_expires_enabled -%} - include h5bp/location/expires.conf; + expires $expires; {% endif -%} {% if h5bp_protect_system_files_enabled -%} @@ -160,6 +226,21 @@ server { {% endblock %} + {% block embed_security -%} + {% if item.value.nginx_embed_security | default(nginx_embed_security | default(true)) -%} + add_header Content-Security-Policy "frame-ancestors 'self'" always; + add_header X-Frame-Options SAMEORIGIN always; + {% endif -%} + {% endblock -%} + + {% block robots_tag_header -%} + {% if robots_tag_header_enabled -%} + # Prevent search engines from indexing non-production environments + add_header X-Robots-Tag "noindex, nofollow" always; + + {% endif -%} + {% endblock -%} + {% block location_php -%} location ~ \.php$ { {% block location_php_basic -%} @@ -168,18 +249,20 @@ server { {% endblock -%} {% block cache_config -%} - {% if item.value.cache is defined and item.value.cache.enabled | default(false) -%} + {% if fastcgi_cache_enabled -%} # Fastcgi cache settings fastcgi_cache wordpress; fastcgi_cache_valid {{ item.value.cache.duration | default(nginx_cache_duration) }}; fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; + fastcgi_cache_background_update {{ item.value.cache.background_update | default(nginx_cache_background_update) }}; {% endif -%} {% endblock -%} {% block fastcgi_basic -%} include fastcgi_params; + fastcgi_param SERVER_NAME $host; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; fastcgi_pass unix:/var/run/php-fpm-wordpress.sock; @@ -211,17 +294,19 @@ server { {% endblock -%} {%- block redirects_domains %} -{% if site_hosts_redirects | default([]) | count %} +{% if site_hosts_redirects | default([]) | count > 0 %} # Redirect some domains {% endif %} {% for host in item.value.site_hosts if host.redirects | default([]) %} server { {% if ssl_enabled -%} - listen [::]:443 ssl http2; - listen 443 ssl http2; + listen [::]:443 ssl; + listen 443 ssl; {% endif -%} listen [::]:80; listen 80; + http2 {{ nginx_http2_enabled | default(false) | ternary('on', 'off') }}; + http3 {{ nginx_http3_enabled | default(false) | ternary('on', 'off') }}; server_name {{ host.redirects | join(' ') }}; {{ self.https() -}} diff --git a/roles/wp-cli/defaults/main.yml b/roles/wp-cli/defaults/main.yml index 8d70ab8527..f4ef131fcd 100644 --- a/roles/wp-cli/defaults/main.yml +++ b/roles/wp-cli/defaults/main.yml @@ -1,6 +1,10 @@ -wp_cli_version: 1.3.0 +gpg2_package: gnupg2 +wp_cli_version: 2.12.0 wp_cli_bin_path: /usr/bin/wp wp_cli_phar_url: "https://github.com/wp-cli/wp-cli/releases/download/v{{ wp_cli_version }}/wp-cli-{{ wp_cli_version }}.phar" +wp_cli_phar_asc_url: "https://github.com/wp-cli/wp-cli/releases/download/v{{ wp_cli_version }}/wp-cli-{{ wp_cli_version }}.phar.asc" +# Note: wp_cli_pgp_public_key must be in binary format +wp_cli_pgp_public_key: "{{ playbook_dir }}/roles/wp-cli/templates/wp-cli.pgp.gpg" wp_cli_completion_url: "https://raw.githubusercontent.com/wp-cli/wp-cli/v{{ wp_cli_version }}/utils/wp-completion.bash" wp_cli_completion_path: /etc/bash_completion.d/wp-completion.bash wp_cli_packages: [] diff --git a/roles/wp-cli/tasks/main.yml b/roles/wp-cli/tasks/main.yml index c3f6770953..10d9c16755 100644 --- a/roles/wp-cli/tasks/main.yml +++ b/roles/wp-cli/tasks/main.yml @@ -1,13 +1,45 @@ --- -- name: Download WP-CLI +- name: Ensure gpg2 is installed + apt: + name: "{{ gpg2_package }}" + state: "{{ gpg2_package_state | default(apt_security_package_state) }}" + cache_valid_time: "{{ apt_cache_valid_time }}" + +- name: Download WP-CLI Phar get_url: url: "{{ wp_cli_phar_url }}" dest: /tmp/wp-cli-{{ wp_cli_version }}.phar +- name: Download WP-CLI Phar Signature + get_url: + url: "{{ wp_cli_phar_asc_url }}" + dest: /tmp/wp-cli-{{ wp_cli_version }}.phar.asc + +- name: Copy WP-CLI release team public key + copy: + src: "{{ wp_cli_pgp_public_key }}" + dest: /tmp/wp-cli.pgp.gpg + mode: '0744' + +- name: Verify WP-CLI + block: + - name: Check GPG signature + command: gpg2 --lock-never --no-default-keyring --keyring /tmp/wp-cli.pgp.gpg --verify /tmp/wp-cli-{{ wp_cli_version }}.phar.asc /tmp/wp-cli-{{ wp_cli_version }}.phar + changed_when: false + rescue: + - name: Delete invalid WP-CLI Phar file + file: + path: "{{ item }}" + state: absent + loop: + - "/tmp/wp-cli-{{ wp_cli_version }}.phar" + - "/tmp/wp-cli-{{ wp_cli_version }}.phar.asc" + - name: Fail verification + fail: + msg: "WP-CLI Phar signature could not be verified. Please try again." + - name: Install WP-CLI command: rsync -c --chmod=0755 --info=name /tmp/wp-cli-{{ wp_cli_version }}.phar {{ wp_cli_bin_path }} - args: - warn: false register: wp_cli changed_when: wp_cli.stdout == 'wp-cli-' + wp_cli_version + '.phar' @@ -15,20 +47,18 @@ command: curl -4Ls {{ wp_cli_completion_url }} -o /tmp/wp-completion-{{ wp_cli_version }}.bash args: creates: /tmp/wp-completion-{{ wp_cli_version }}.bash - warn: false - name: Install WP-CLI tab completions command: rsync -c --chmod=0644 --info=name /tmp/wp-completion-{{ wp_cli_version }}.bash {{ wp_cli_completion_path }} - args: - warn: false register: wp_cli_completion changed_when: wp_cli_completion.stdout == 'wp-completion-' + wp_cli_version + '.bash' - name: Install WP-CLI packages command: wp package install {{ item }} + become: true become_user: "{{ web_user }}" register: wp_cli_packages_installed changed_when: - "'Nothing to install or update' not in wp_cli_packages_installed.stdout" - "'Package operations: 0 installs, 0 updates, 0 removals' not in wp_cli_packages_installed.stdout" - with_items: "{{ wp_cli_packages }}" + loop: "{{ wp_cli_packages }}" diff --git a/roles/wp-cli/templates/wp-cli.pgp.gpg b/roles/wp-cli/templates/wp-cli.pgp.gpg new file mode 100644 index 0000000000..dfc7b7bca0 Binary files /dev/null and b/roles/wp-cli/templates/wp-cli.pgp.gpg differ diff --git a/roles/xdebug-tunnel/defaults/main.yml b/roles/xdebug-tunnel/defaults/main.yml index c9a52a144a..3623b4ac55 100644 --- a/roles/xdebug-tunnel/defaults/main.yml +++ b/roles/xdebug-tunnel/defaults/main.yml @@ -1,10 +1,10 @@ -xdebug_tunnel_remote_port: 9000 +xdebug_tunnel_remote_port: 9003 xdebug_tunnel_host: localhost -xdebug_tunnel_local_port: 9000 +xdebug_tunnel_local_port: 9003 xdebug_tunnel_control_socket: /tmp/trellis-xdebug-{{ xdebug_tunnel_inventory_host }} xdebug_tunnel_control_identity: "{{ ansible_user_id }}" xdebug_tunnel_port_mapping: "{{ xdebug_tunnel_remote_port }}:{{ xdebug_tunnel_host }}:{{ xdebug_tunnel_local_port }}" xdebug_tunnel_ssh_user: "{{ hostvars[xdebug_tunnel_inventory_host]['ansible_user'] | default(admin_user) }}" -xdebug_tunnel_ssh_host: "{{ hostvars[xdebug_tunnel_inventory_host]['ansible_host'] | default(xdebug_tunnel_inventory_host) }}" +xdebug_tunnel_ssh_host: "{{ hostvars[xdebug_tunnel_inventory_host]['ansible_default_ipv4']['address'] | default(xdebug_tunnel_inventory_host) }}" xdebug_tunnel_user_at_host: "{{ xdebug_tunnel_ssh_user }}@{{ xdebug_tunnel_ssh_host }}" diff --git a/roles/xdebug-tunnel/tasks/main.yml b/roles/xdebug-tunnel/tasks/main.yml index b015c11099..84ec403faf 100644 --- a/roles/xdebug-tunnel/tasks/main.yml +++ b/roles/xdebug-tunnel/tasks/main.yml @@ -2,14 +2,15 @@ - name: Create or close Xdebug SSH tunnel command: | {% if xdebug_remote_enable | bool %} - ssh -M -S '{{ xdebug_tunnel_control_socket }}' -fnNT -R {{ xdebug_tunnel_port_mapping }} {{ xdebug_tunnel_user_at_host}} '{{ xdebug_tunnel_control_identity }}' + ssh -M -S '{{ xdebug_tunnel_control_socket }}' -fnNT -R {{ xdebug_tunnel_port_mapping }} {{ xdebug_tunnel_user_at_host }} '{{ xdebug_tunnel_control_identity }}' {% else %} ssh -S '{{ xdebug_tunnel_control_socket }}' -O exit '{{ xdebug_tunnel_control_identity }}' {% endif %} - connection: local + delegate_to: localhost become: no register: xdebug_tunnel ignore_errors: true + changed_when: true - name: Interpret and present Xdebug SSH tunnel errors fail: @@ -20,9 +21,9 @@ SSH tunnel already closed! {% endif %} {{ xdebug_tunnel.stderr | default('Unknown error in handling Xdebug SSH tunnel') }} - when: xdebug_tunnel | failed or 'already' in xdebug_tunnel.stderr | default('') + when: xdebug_tunnel is failed or 'already' in xdebug_tunnel.stderr | default('') - name: Announce Xdebug SSH tunnel status debug: msg: SSH Tunnel was {{ xdebug_remote_enable | bool | ternary('created', 'closed') }}! - when: xdebug_tunnel | changed + when: xdebug_tunnel is changed diff --git a/roles/xdebug/defaults/main.yml b/roles/xdebug/defaults/main.yml index 334b958b8c..ff80d8fba9 100644 --- a/roles/xdebug/defaults/main.yml +++ b/roles/xdebug/defaults/main.yml @@ -1,11 +1,15 @@ +# XDebug Generic +xdebug_output_dir: /tmp +xdebug_trigger_value: + # XDebug Remote Debugging -xdebug_remote_enable: 0 -xdebug_remote_connect_back: 0 -xdebug_remote_host: localhost -xdebug_remote_port: 9000 -xdebug_remote_log: /tmp/xdebug.log +xdebug_mode: 'off' +xdebug_start_with_request: 'no' +xdebug_discover_client_host: 0 +xdebug_client_host: localhost +xdebug_client_port: 9003 +xdebug_log: /tmp/xdebug.log xdebug_idekey: XDEBUG -xdebug_extended_info: 1 xdebug_max_nesting_level: 200 # XDebug Display Settings @@ -18,24 +22,13 @@ xdebug_var_display_max_depth: 3 # XDebug Function/Stack Traces xdebug_collect_assignments: 0 -xdebug_collect_includes: 1 -xdebug_collect_params: 0 xdebug_collect_return: 0 -xdebug_collect_vars: 0 xdebug_show_exception_trace: 0 xdebug_show_local_vars: 0 -xdebug_show_mem_delta: 0 -xdebug_trace_enable_trigger: 0 -xdebug_trace_enable_trigger_value: xdebug_trace_format: 0 xdebug_trace_options: 0 -xdebug_trace_output_dir: /tmp xdebug_trace_output_name: trace.%c # XDebug Profiler xdebug_profiler_append: 0 -xdebug_profiler_enable: 0 -xdebug_profiler_enable_trigger: 0 -xdebug_profiler_enable_trigger_value: -xdebug_profiler_output_dir: /tmp xdebug_profiler_output_name: cachegrind.out.%p diff --git a/roles/xdebug/tasks/main.yml b/roles/xdebug/tasks/main.yml index 00fc2290a3..f4d1d3acef 100644 --- a/roles/xdebug/tasks/main.yml +++ b/roles/xdebug/tasks/main.yml @@ -1,35 +1,35 @@ --- -- block: - - name: Install Xdebug - apt: - name: php-xdebug - state: latest - update_cache: true - cache_valid_time: "{{ apt_cache_valid_time }}" +- name: Import PHP version specific vars + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - '{{ php_version }}.yml' # e.g. 8.2.yml + - version-specific-defaults.yml + paths: + - "{{ playbook_dir }}/roles/php/vars/" - - name: Template the Xdebug configuration file - template: - src: xdebug.ini.j2 - dest: /etc/php/7.1/mods-available/xdebug.ini - notify: reload php-fpm +- name: Install Xdebug + apt: + name: "{{ php_xdebug_package }}" + state: "{{ php_xdebug_package_state | default(apt_dev_package_state) }}" + cache_valid_time: "{{ apt_cache_valid_time }}" - - name: Ensure 20-xdebug.ini is present - file: - src: /etc/php/7.1/mods-available/xdebug.ini - dest: /etc/php/7.1/fpm/conf.d/20-xdebug.ini - state: link - notify: reload php-fpm - - when: xdebug_remote_enable | bool +- name: Template the Xdebug configuration file + template: + src: xdebug.ini.j2 + dest: /etc/php/{{ php_version }}/mods-available/xdebug.ini + mode: '0644' + notify: reload php-fpm -- name: Disable Xdebug +- name: Ensure 20-xdebug.ini is present file: - path: /etc/php/7.1/fpm/conf.d/20-xdebug.ini - state: absent - when: not xdebug_remote_enable | bool + src: /etc/php/{{ php_version }}/mods-available/xdebug.ini + dest: /etc/php/{{ php_version }}/fpm/conf.d/20-xdebug.ini + state: link notify: reload php-fpm - name: Disable Xdebug CLI file: - path: /etc/php/7.1/cli/conf.d/20-xdebug.ini + path: /etc/php/{{ php_version }}/cli/conf.d/20-xdebug.ini state: absent diff --git a/roles/xdebug/templates/xdebug.ini.j2 b/roles/xdebug/templates/xdebug.ini.j2 index 72435a2bf8..fb8a18c951 100644 --- a/roles/xdebug/templates/xdebug.ini.j2 +++ b/roles/xdebug/templates/xdebug.ini.j2 @@ -3,15 +3,18 @@ [XDebug] zend_extension=xdebug.so +; Generic +xdebug.output_dir={{ xdebug_output_dir }} +xdebug.trigger_value={{ xdebug_trigger_value }} + ; Remote Debugging -xdebug.remote_enable={{ xdebug_remote_enable }} -xdebug.remote_connect_back={{ xdebug_remote_connect_back }} -xdebug.remote_host={{ xdebug_remote_host }} -xdebug.remote_port={{ xdebug_remote_port }} -xdebug.remote_handler=dbgp -xdebug.remote_log={{ xdebug_remote_log }} +xdebug.mode={{ xdebug_mode }} +xdebug.start_with_request={{ xdebug_start_with_request }} +xdebug.discover_client_host={{ xdebug_discover_client_host }} +xdebug.client_host={{ xdebug_client_host }} +xdebug.client_port={{ xdebug_client_port }} +xdebug.log={{ xdebug_log }} xdebug.idekey={{ xdebug_idekey }} -xdebug.extended_info={{ xdebug_extended_info }} xdebug.max_nesting_level={{ xdebug_max_nesting_level }} ; Display Settings @@ -24,24 +27,13 @@ xdebug.var_display_max_depth={{ xdebug_var_display_max_depth }} ; Function/Stack Traces xdebug.collect_assignments={{ xdebug_collect_assignments }} -xdebug.collect_includes={{ xdebug_collect_includes }} -xdebug.collect_params={{ xdebug_collect_params }} xdebug.collect_return={{ xdebug_collect_return }} -xdebug.collect_vars={{ xdebug_collect_vars }} xdebug.show_exception_trace={{ xdebug_show_exception_trace }} xdebug.show_local_vars={{ xdebug_show_local_vars }} -xdebug.show_mem_delta={{ xdebug_show_mem_delta }} -xdebug.trace_enable_trigger={{ xdebug_trace_enable_trigger }} -xdebug.trace_enable_trigger_value={{ xdebug_trace_enable_trigger_value }} xdebug.trace_format={{ xdebug_trace_format }} xdebug.trace_options={{ xdebug_trace_options }} -xdebug.trace_output_dir={{ xdebug_trace_output_dir }} xdebug.trace_output_name={{ xdebug_trace_output_name }} ; Profiler xdebug.profiler_append={{ xdebug_profiler_append }} -xdebug.profiler_enable={{ xdebug_profiler_enable }} -xdebug.profiler_enable_trigger={{ xdebug_profiler_enable_trigger }} -xdebug.profiler_enable_trigger_value={{ xdebug_profiler_enable_trigger_value }} -xdebug.profiler_output_dir={{ xdebug_profiler_output_dir }} xdebug.profiler_output_name={{ xdebug_profiler_output_name }} diff --git a/rollback.yml b/rollback.yml index f3e0405a92..47a01bbc38 100644 --- a/rollback.yml +++ b/rollback.yml @@ -14,10 +14,5 @@ - name: Rollback a Deploy hosts: web:&{{ env }} remote_user: "{{ web_user }}" - - vars: - project_root: "{{ www_root }}/{{ site }}" - project_current_path: "{{ wordpress_sites[site].current_path | default('current') }}" - roles: - rollback diff --git a/server.yml b/server.yml index bd181f1f96..47da811172 100644 --- a/server.yml +++ b/server.yml @@ -9,34 +9,26 @@ roles: - { role: connection, tags: [connection, always] } -- name: Install prerequisites - hosts: web:&{{ env }} - gather_facts: false - become: yes - tasks: - - name: Install Python 2.x - raw: which python || sudo apt-get update && sudo apt-get install -qq -y python-simplejson - register: python_check - changed_when: not python_check.stdout | search('/usr/bin/python') - -- name: WordPress Server - Install LEMP Stack with PHP 7.1 and MariaDB MySQL +- name: WordPress Server - Install LEMP Stack with PHP and MariaDB MySQL hosts: web:&{{ env }} become: yes roles: - { role: common, tags: [common] } - - { role: swapfile, swapfile_size: 1GB, tags: [swapfile] } + - { role: swapfile, swapfile_size: 1GB, swapfile_file: /swapfile, tags: [swapfile] } - { role: fail2ban, tags: [fail2ban] } - - { role: ferm, tags: [ferm] } + - { role: ferm, tags: [ferm, letsencrypt] } + - { role: hosts, tags: [hosts] } - { role: ntp, tags: [ntp] } - { role: users, tags: [users] } - { role: sshd, tags: [sshd] } - { role: mariadb, tags: [mariadb] } - { role: ssmtp, tags: [ssmtp, mail] } - { role: php, tags: [php] } - - { role: memcached, tags: [memcached] } + - { role: memcached, tags: [memcached], when: sites_using_memcached | count > 0 } + - { role: redis, tags: [redis], when: sites_using_redis | count > 0 } - { role: nginx, tags: [nginx] } - { role: logrotate, tags: [logrotate] } - { role: composer, tags: [composer] } - { role: wp-cli, tags: [wp-cli] } - - { role: letsencrypt, tags: [letsencrypt], when: sites_using_letsencrypt | count } + - { role: letsencrypt, tags: [letsencrypt], when: sites_using_letsencrypt | count > 0 } - { role: wordpress-setup, tags: [wordpress, wordpress-setup, letsencrypt] } diff --git a/trellis.cli.yml b/trellis.cli.yml new file mode 100644 index 0000000000..7cfe2470b0 --- /dev/null +++ b/trellis.cli.yml @@ -0,0 +1,6 @@ +# Trellis CLI example config file +# https://roots.io/trellis/docs/cli/#configuration +# +# database_app: sequel-ace +# open: +# admin: "https://mysite.com/wp/wp-admin" diff --git a/vagrant.default.yml b/vagrant.default.yml deleted file mode 100644 index 53a06b2716..0000000000 --- a/vagrant.default.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -vagrant_ip: '192.168.50.5' -vagrant_cpus: 1 -vagrant_memory: 1024 # in MB -vagrant_box: 'bento/ubuntu-16.04' -vagrant_box_version: '<= 2.3.8' -vagrant_ansible_version: '2.4.0' -vagrant_skip_galaxy: false - -vagrant_install_plugins: true -vagrant_plugins: - - name: vagrant-bindfs - - name: vagrant-hostmanager - -# Array of synced folders: -# - local_path: . -# destination: /path/on/vm -# create: false -# type: nfs -# bindfs: true -# mount_options: [] -# bindfs_options: {} -# See https://www.vagrantup.com/docs/synced-folders/basic_usage.html#mount_options -vagrant_synced_folders: [] diff --git a/xdebug-tunnel.yml b/xdebug-tunnel.yml index 355a334b06..014c1f0d41 100644 --- a/xdebug-tunnel.yml +++ b/xdebug-tunnel.yml @@ -9,10 +9,11 @@ hosts: "{{ xdebug_tunnel_inventory_host }}" become: yes roles: + - { role: sshd, tags: [sshd] } - { role: xdebug, tags: [xdebug] } - { role: xdebug-tunnel, tags: [xdebug-tunnel] } handlers: - name: reload php-fpm service: - name: php7.1-fpm + name: php{{ php_version }}-fpm state: reloaded