diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index ec6ac6af71..425c793cf0 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,9 +1,11 @@ ## 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f569d852..0cca88dc7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ### HEAD +* Bump Ansible `version_tested_max` to 2.4.2.0 ([#932](https://github.com/roots/trellis/pull/932)) +* Add MariaDB 10.2 PPA ([#926](https://github.com/roots/trellis/pull/926)) * Switch from `.dev` to `.test` ([#923](https://github.com/roots/trellis/pull/923)) ### 1.0.0-rc.2: November 13th, 2017 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..f663c4792c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at team@roots.io. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/README.md b/README.md index 51dc0dbdf3..acf03bd7f5 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ Ansible playbooks for setting up a LEMP stack for WordPress. Trellis will configure a server with the following and more: -* Ubuntu 14.04 Trusty LTS +* Ubuntu 16.04 Xenial LTS * Nginx (with optional FastCGI micro-caching) -* PHP 7.0 +* 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 @@ -26,15 +26,16 @@ Trellis will configure a server with the following and more: * 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: -* [Ansible](http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-pip) 2.0.2 * [Virtualbox](https://www.virtualbox.org/wiki/Downloads) >= 4.3.10 -* [Vagrant](http://www.vagrantup.com/downloads.html) >= 1.5.4 -* [vagrant-bindfs](https://github.com/gael-ian/vagrant-bindfs#installation) >= 0.3.1 (Windows users may skip this) -* [vagrant-hostmanager](https://github.com/smdahlen/vagrant-hostmanager#installation) +* [Vagrant](https://www.vagrantup.com/downloads.html) >= 1.8.5 ## Installation @@ -54,7 +55,6 @@ See a complete working example in the [roots-example-project.com repo](https://g 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` -4. Install the Ansible Galaxy roles: `$ cd trellis && ansible-galaxy install -r requirements.yml` 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). @@ -73,7 +73,9 @@ Additional pt-ops documentation is available at [Google Docs](https://drive.goog ## Remote server setup (staging/production) -A base Ubuntu 14.04 server is required for setting up remote servers. +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//wordpress_sites.yml` and in `group_vars//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/` @@ -82,6 +84,15 @@ A base Ubuntu 14.04 server is required for setting up remote servers. [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//wordpress_sites.yml` file +2. Set the `branch` you want to deploy +3. Run `./bin/deploy.sh ` +4. To rollback a deploy, run `ansible-playbook rollback.yml -e "site= env="` + +[Read the deploys docs](https://roots.io/trellis/docs/deploys/) for more information. + ## Community Keep track of development and community news. diff --git a/Vagrantfile b/Vagrantfile index 0a54a52130..a568e0e67d 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,204 +1,166 @@ # -*- mode: ruby -*- # vi: set ft=ruby : -require 'yaml' - -ip = '192.168.51.62' # pick any local IP -cpus = 1 -memory = 1024 # in MB - -ANSIBLE_PATH = __dir__ # absolute path to Ansible directory +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 -# 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')}" +require File.join(ANSIBLE_PATH, 'lib', 'trellis', 'vagrant') +require File.join(ANSIBLE_PATH, 'lib', 'trellis', 'config') +require 'yaml' -wp_config_file = File.join(ANSIBLE_PATH, 'group_vars', 'development', 'wordpress_sites.yml') -static_config_file = File.join(ANSIBLE_PATH, 'group_vars', 'development', 'static_sites.yml') -main_config_file = File.join(ANSIBLE_PATH, 'group_vars', 'all', 'main.yml') +vconfig = YAML.load_file("#{ANSIBLE_PATH}/vagrant.default.yml") -def fail_with_message(msg) - fail Vagrant::Errors::VagrantError.new, msg +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 -if File.exists?(wp_config_file) - wordpress_sites = YAML.load_file(wp_config_file)['wordpress_sites'] - static_sites = YAML.load_file(static_config_file)['static_sites'] - www_root = YAML.load_file(main_config_file)['www_root'] - fail_with_message "No sites found in #{wp_config_file}." if wordpress_sites.to_h.empty? -else - fail_with_message "#{wp_config_file} was not found. Please set `ANSIBLE_PATH` in your Vagrantfile." -end +ensure_plugins(vconfig.fetch('vagrant_plugins')) if vconfig.fetch('vagrant_install_plugins') -if !Dir.exists?(ENV['ANSIBLE_ROLES_PATH']) && !Vagrant::Util::Platform.windows? - fail_with_message "You are missing the required Ansible Galaxy roles, please install them with this command:\nansible-galaxy install -r requirements.yml" -end +trellis_config = Trellis::Config.new(root_path: ANSIBLE_PATH) -Vagrant.require_version '>= 1.5.1' +Vagrant.require_version '>= 1.8.5' Vagrant.configure('2') do |config| - config.vm.box = 'ubuntu/trusty64' + 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'} - if Vagrant.has_plugin? 'vagrant-hostmanager' - config.hostmanager.enabled = true - config.hostmanager.manage_host = true + # 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 - fail_with_message "vagrant-hostmanager missing, please install the plugin with this command:\nvagrant plugin install vagrant-hostmanager" + config.vm.network :private_network, ip: vconfig.fetch('vagrant_ip'), hostsupdater: 'skip' end - ############################################################################ - # local_as devbox definition - ############################################################################ - config.vm.define "local_as", primary: true do |local_as| + main_hostname, *hostnames = trellis_config.site_hosts_canonical + config.vm.hostname = main_hostname - # Required for NFS to work - local_as.vm.network :private_network, ip: ip, hostsupdater: 'skip' + if Vagrant.has_plugin?('vagrant-hostmanager') && !trellis_config.multisite_subdomains? + redirects = trellis_config.site_hosts_redirects - # disable default mount - local_as.vm.synced_folder ".", "/vagrant", id: "vagrant-root", disabled: true - - hostname, *aliases = wordpress_sites.flat_map { |(_name, site)| site['site_hosts'] } - local_as.vm.hostname = hostname - aliases.concat static_sites.flat_map { |(_name, site)| site['site_hosts'] } # add aliases from the static sites - www_aliases = ["www.#{hostname}"] + aliases.map { |host| "www.#{host}" } - - local_as.hostmanager.aliases = (aliases + www_aliases).uniq + 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' - wordpress_sites.each_pair do |name, site| - local_as.vm.synced_folder local_site_path(site), remote_site_path(name), owner: 'vagrant', group: 'www-data', mount_options: ['dmode=776', 'fmode=775'] - end - local_as.vm.synced_folder File.join(ANSIBLE_PATH, 'hosts'), File.join(ANSIBLE_PATH.sub(__dir__, '/vagrant'), 'hosts'), mount_options: ['dmode=755', 'fmode=644'] - 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 - wordpress_sites.merge(static_sites).each_pair do |name, site| - local_as.vm.synced_folder local_site_path(site), nfs_path(name), type: 'nfs' - local_as.bindfs.bind_folder nfs_path(name), remote_site_path(name), u: 'vagrant', g: 'www-data', o: 'nonempty' - end - end + 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 - if Vagrant::Util::Platform.windows? - local_as.vm.provision :shell do |sh| - sh.path = File.join(ANSIBLE_PATH, 'windows.sh') - sh.args = [Vagrant::VERSION] - 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 - local_as.vm.provision :ansible do |ansible| - ansible.playbook = File.join(ANSIBLE_PATH, 'dev.yml') - ansible.vault_password_file = "../vault-key" - ansible.groups = { - 'web' => ['local_as'], - 'development' => ['local_as'] - } - - 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 + 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 - # Vagrant Triggers - # https://github.com/emyl/vagrant-triggers - # - # If the vagrant-triggers plugin is installed, we can run various scripts on Vagrant - # state changes like `vagrant up`, `vagrant halt`, `vagrant suspend`, and `vagrant destroy` - # - # These scripts are run on the host machine, so we use `vagrant ssh` to tunnel back - # into the VM and execute things. - if Vagrant.has_plugin? 'vagrant-triggers' - local_as.trigger.before [:halt, :destroy], :stdout => true do - wordpress_sites.each_key do |wp_site_folder| - info "Exporting db for #{wp_site_folder}" - run_remote "cd #{www_root}/#{wp_site_folder}/ && wp db export --allow-root" - end - end - else - fail_with_message "vagrant-triggers missing, please install the plugin with this command:\nvagrant plugin install vagrant-triggers" + 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 - ############################################################################ - # sandbox definition - ############################################################################ - config.vm.define "sandbox", autostart: false do |sandbox| - sandbox.vm.network :private_network, ip: '192.168.51.63' - sandbox.vm.hostname = "sandbox.proteusthemes.test" - - if Vagrant.has_plugin? 'vagrant-hostmanager' - sandbox.hostmanager.enabled = true - sandbox.hostmanager.manage_host = true - sandbox.hostmanager.aliases = ['xml-io.proteusthemes.test'] + 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 - sandbox.vm.provision :ansible do |ansible| - ansible.playbook = File.join(ANSIBLE_PATH, 'dev.yml') - ansible.groups = { - 'web' => ['sandbox'], - 'sandbox' => ['sandbox'] - } - - 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 + 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', cpus] - vb.customize ['modifyvm', :id, '--memory', memory] + vb.customize ['modifyvm', :id, '--cpus', vconfig.fetch('vagrant_cpus')] + vb.customize ['modifyvm', :id, '--memory', vconfig.fetch('vagrant_memory')] + vb.customize ['modifyvm', :id, '--ioapic', vconfig.fetch('vagrant_ioapic', 'on')] # Fix for slow external network connections - vb.customize ['modifyvm', :id, '--natdnshostresolver1', 'on'] - vb.customize ['modifyvm', :id, '--natdnsproxy1', 'on'] + vb.customize ['modifyvm', :id, '--natdnshostresolver1', vconfig.fetch('vagrant_natdnshostresolver', 'on')] + vb.customize ['modifyvm', :id, '--natdnsproxy1', vconfig.fetch('vagrant_natdnsproxy', 'on')] end # VMware Workstation/Fusion settings ['vmware_fusion', 'vmware_workstation'].each do |provider| config.vm.provider provider do |vmw, override| - override.vm.box = 'puppetlabs/ubuntu-14.04-64-nocm' vmw.name = config.vm.hostname - vmw.vmx['numvcpus'] = cpus - vmw.vmx['memsize'] = memory + 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| - override.vm.box = 'parallels/ubuntu-14.04' prl.name = config.vm.hostname - prl.cpus = cpus - prl.memory = memory + prl.cpus = vconfig.fetch('vagrant_cpus') + prl.memory = vconfig.fetch('vagrant_memory') + prl.update_guest_tools = true end - -end - -def local_site_path(site) - File.expand_path(site['local_path'], ANSIBLE_PATH) -end - -def nfs_path(site_name) - "/vagrant-nfs-#{site_name}" -end - -def remote_site_path(site_name) - "/opt/proteusnet/www/#{site_name}" end diff --git a/ansible.cfg b/ansible.cfg index ef094a9583..6d3f306b09 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -13,3 +13,4 @@ vault_password_file = ../vault-key [ssh_connection] ssh_args = -o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s pipelining = True +retries = 1 diff --git a/bin/deploy.sh b/bin/deploy.sh new file mode 100755 index 0000000000..79d72c37d4 --- /dev/null +++ b/bin/deploy.sh @@ -0,0 +1,45 @@ +#!/bin/bash +shopt -s nullglob + +ENVIRONMENTS=( hosts/* ) +ENVIRONMENTS=( "${ENVIRONMENTS[@]##*/}" ) + +show_usage() { + echo "Usage: deploy [options] + + is the environment to deploy to ("staging", "production", etc) + 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 new file mode 100755 index 0000000000..8f475677ac --- /dev/null +++ b/bin/xdebug-tunnel.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +show_usage() { + echo " +Usage: bin/xdebug-tunnel.sh + + can be 'open' or 'close' + is the hostname, IP, or inventory alias in your \`hosts/\` 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 argument '${1}' is not acceptable." + show_usage + exit 1 +fi + +if [[ -z $2 ]]; then + >&2 echo "The 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/dev.yml b/dev.yml index 88fea598db..f2682ca5b0 100644 --- a/dev.yml +++ b/dev.yml @@ -1,5 +1,5 @@ --- -- name: "WordPress Server: Install LEMP Stack with PHP 7.0 and MariaDB MySQL" +- name: "WordPress Server: Install LEMP Stack with PHP 7.1 and MariaDB MySQL" hosts: web:&development become: yes remote_user: vagrant @@ -9,12 +9,13 @@ - { role: dotfiles, tags: [dotfiles], become: no } - { role: fail2ban, tags: [fail2ban] } - { role: ferm, tags: [ferm] } - - { role: ntp } + - { role: ntp, tags: [ntp] } - { role: sshd, tags: [sshd] } - { role: mariadb, tags: [mariadb] } - - { role: ssmtp, tags: [ssmtp mail] } - - { role: mailhog, tags: [mailhog mail] } + - { role: mailhog, tags: [mailhog, mail] } - { role: php, tags: [php] } + # - { role: xdebug, tags: [php, xdebug] } + # - { role: memcached, tags: [memcached] } - { role: nginx, tags: [nginx] } - { role: logrotate, tags: [logrotate] } - { role: composer, tags: [composer] } diff --git a/group_vars/all/helpers.yml b/group_vars/all/helpers.yml new file mode 100644 index 0000000000..1e56dcd9db --- /dev/null +++ b/group_vars/all/helpers.yml @@ -0,0 +1,19 @@ +wordpress_env_defaults: + db_host: localhost + db_name: "{{ item.key | underscore }}_{{ env }}" + db_user: "{{ item.key | underscore }}" + db_user_host: localhost + disable_wp_cron: false + wp_env: "{{ env }}" + wp_home: "{{ ssl_enabled | ternary('https', 'http') }}://{{ site_hosts_canonical | first }}" + wp_siteurl: "{{ ssl_enabled | ternary('https', 'http') }}://{{ site_hosts_canonical | first }}" + domain_current_site: "{{ site_hosts_canonical | first }}" + +site_env: "{{ wordpress_env_defaults | combine(item.value.env | default({}), vault_wordpress_sites[item.key].env) }}" +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))) }}" diff --git a/group_vars/all/known_hosts.yml b/group_vars/all/known_hosts.yml new file mode 100644 index 0000000000..b8af937ca5 --- /dev/null +++ b/group_vars/all/known_hosts.yml @@ -0,0 +1,18 @@ +# Documentation: https://roots.io/trellis/docs/troubleshooting/#composer-install-host-key-verification-failed + +# Host keys to add to known_hosts, e.g., +# - git host for Bedrock-based project (`repo` variable in `wordpress_sites`) +# - git hosts in Bedrock project's composer.json +known_hosts: + - name: github.com + key: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== + - name: bitbucket.org + key: bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw== + - name: gitlab.com + key: gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf + - name: gitlab.com + key: gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9 + +# Whether to automatically accept the host key for your `repo` (in `wordpress_sites`) during git clone. +# To avoid man-in-the-middle attacks, set this to `false` and add repo host key to `known_hosts` above. +repo_accept_hostkey: true diff --git a/group_vars/all/main.yml b/group_vars/all/main.yml index 05784b92d9..3574d567ec 100644 --- a/group_vars/all/main.yml +++ b/group_vars/all/main.yml @@ -1,23 +1,35 @@ composer_keep_updated: true composer_global_packages: - { name: hirak/prestissimo } -apt_cache_valid_time: 86400 -default_timezone: Etc/UTC +apt_cache_valid_time: 3600 +apt_package_state: present +apt_security_package_state: latest +apt_dev_package_state: latest +ntp_timezone: Etc/UTC +ntp_manage_config: true www_root: /opt/proteusnet/www -scripts_root: /opt/proteusnet/scripts ip_whitelist: - - "{{ lookup('pipe', 'curl -4 -s https://api.ipify.org') }}" - -wordpress_env_defaults: - db_host: localhost - db_name: "{{ item.key | underscore }}_{{ env }}" - db_user: "{{ item.key | underscore }}" - disable_wp_cron: false - wp_env: "{{ env }}" - wp_home: "{{ item.value.ssl.enabled | default(false) | ternary('https', 'http') }}://${HTTP_HOST}" - wp_siteurl: "${WP_HOME}" + - "{{ (env == 'development') | ternary(ansible_default_ipv4.gateway, ipify_public_ip | default('')) }}" -site_env: "{{ wordpress_env_defaults | combine(item.value.env | default({}), vault_wordpress_sites[item.key].env) }}" +scripts_root: /opt/proteusnet/scripts mariadb_mirror: ams2.mirrors.digitalocean.com mariadb_version: "10.1" + +apt_packages_custom: + zip: "{{ apt_package_state }}" + unzip: "{{ apt_package_state }}" + imagemagick: "{{ apt_package_state }}" + acl: "{{ apt_package_state }}" + + +# 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: +# - vault_wordpress_sites.*.*_salt -- matches vault_wordpress_sites.example.com.env.secure_auth_salt etc. +# Will not function for var names or topmost dict keys that contain a period ('.'). +raw_vars: + - vault_mail_password + - vault_mysql_root_password + - vault_users.*.password + - vault_users.*.salt + - vault_wordpress_sites diff --git a/group_vars/all/users.yml b/group_vars/all/users.yml index 12fe286957..2150eddd24 100644 --- a/group_vars/all/users.yml +++ b/group_vars/all/users.yml @@ -1,7 +1,7 @@ # Documentation: https://roots.io/trellis/docs/ssh-keys/ admin_user: "{{ vault_admin_user }}" -# Also define 'vault_sudoer_passwords' (`group_vars/staging/vault.yml`, `group_vars/production/vault.yml`) +# Also define 'vault_users' (`group_vars/staging/vault.yml`, `group_vars/production/vault.yml`) users: - name: "{{ web_user }}" groups: @@ -38,4 +38,4 @@ users: web_user: "{{ vault_web_user }}" web_group: "{{ vault_web_group }}" web_sudoers: - - "/usr/sbin/service php7.0-fpm *" + - "/usr/sbin/service php7.1-fpm *" diff --git a/group_vars/development/mail.yml b/group_vars/development/mail.yml index 40d8208b4e..f6d4f663d5 100644 --- a/group_vars/development/mail.yml +++ b/group_vars/development/mail.yml @@ -1,8 +1,2 @@ # Documentation: https://roots.io/trellis/docs/mail/ -mailhog_install_ssmtp: no -mail_admin: admin@proteusthemes.test -mail_hostname: proteusthemes.test -mail_smtp_server: localhost:1025 -ssmtp_auth_method: "" -ssmtp_start_tls: 'no' -ssmtp_tls: 'no' +php_sendmail_path: "{{ mailhog_install_dir }}/mhsendmail" diff --git a/group_vars/development/php.yml b/group_vars/development/php.yml index 7dd15a2b9a..7b9af47888 100644 --- a/group_vars/development/php.yml +++ b/group_vars/development/php.yml @@ -5,11 +5,5 @@ php_track_errors: 'On' php_mysqlnd_collect_memory_statistics: 'On' php_opcache_enable: 0 -xdebug_install: false -php_xdebug_remote_enable: true -php_xdebug_remote_connect_back: true -php_xdebug_remote_host: localhost -php_xdebug_remote_port: 9000 -php_xdebug_remote_log: /tmp/xdebug.log -php_xdebug_idekey: XDEBUG -php_max_nesting_level: 200 +xdebug_remote_enable: 1 +xdebug_remote_connect_back: 1 diff --git a/group_vars/development/wordpress_sites.yml b/group_vars/development/wordpress_sites.yml index adc300d49a..a1d46930d2 100644 --- a/group_vars/development/wordpress_sites.yml +++ b/group_vars/development/wordpress_sites.yml @@ -4,7 +4,7 @@ wordpress_sites: demo.proteusthemes.com: site_hosts: - - demo.proteusthemes.test + - canonical: demo.proteusthemes.test local_path: ../www/demo.proteusthemes.com # path targeting local Bedrock site directory (relative to Ansible root) site_title: ProteusThemes admin_user: proteusnet @@ -27,7 +27,7 @@ wordpress_sites: db_prefix: wpsites_ www.proteusthemes.com: site_hosts: - - www.proteusthemes.test + - canonical: www.proteusthemes.test local_path: ../www/www.proteusthemes.com site_title: ProteusThemes admin_user: proteusnet diff --git a/group_vars/production/wordpress_sites.yml b/group_vars/production/wordpress_sites.yml index 34ae206b47..eca719942d 100644 --- a/group_vars/production/wordpress_sites.yml +++ b/group_vars/production/wordpress_sites.yml @@ -5,7 +5,7 @@ wordpress_sites: demo.proteusthemes.com: site_hosts: - - demo.proteusthemes.com + - canonical: demo.proteusthemes.com site_title: ProteusThemes Demos admin_user: "{{ vault_wordpress_sites['demo.proteusthemes.com'].admin_user }}" admin_password: "{{ vault_wordpress_sites['demo.proteusthemes.com'].admin_password }}" @@ -26,7 +26,7 @@ wordpress_sites: db_prefix: "{{ vault_wordpress_sites['demo.proteusthemes.com'].env.db_prefix }}" www.proteusthemes.com: site_hosts: - - www.proteusthemes.com + - canonical: www.proteusthemes.com site_title: ProteusThemes admin_user: "{{ vault_wordpress_sites['www.proteusthemes.com'].admin_user }}" admin_password: "{{ vault_wordpress_sites['www.proteusthemes.com'].admin_password }}" @@ -46,7 +46,7 @@ wordpress_sites: db_prefix: "{{ vault_wordpress_sites['www.proteusthemes.com'].env.db_prefix }}" olm.proteusthemes.com: site_hosts: - - olm.proteusthemes.com + - canonical: olm.proteusthemes.com site_title: ProteusThemes admin_user: "{{ vault_wordpress_sites['olm.proteusthemes.com'].admin_user }}" admin_password: "{{ vault_wordpress_sites['olm.proteusthemes.com'].admin_password }}" diff --git a/lib/trellis/config.rb b/lib/trellis/config.rb new file mode 100644 index 0000000000..5dae571c70 --- /dev/null +++ b/lib/trellis/config.rb @@ -0,0 +1,76 @@ +# 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 index 3839606336..9bb2cbede6 100644 --- a/lib/trellis/plugins/callback/output.py +++ b/lib/trellis/plugins/callback/output.py @@ -5,7 +5,6 @@ import os.path import sys -from ansible.parsing.dataloader import DataLoader from ansible.plugins.callback.default import CallbackModule as CallbackModule_default try: @@ -27,6 +26,7 @@ class CallbackModule(CallbackModule_default): 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 @@ -58,23 +58,22 @@ 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 - loader = DataLoader() - play_vars = play.get_variable_manager().get_vars(loader=loader, play=play) - if 'vagrant_version' in play_vars: - self.vagrant_version = play_vars['vagrant_version'] + extra_vars = play.get_variable_manager().extra_vars + if 'vagrant_version' in extra_vars: + self.vagrant_version = extra_vars['vagrant_version'] - def v2_playbook_item_on_ok(self, result): + def v2_runner_item_on_ok(self, result): output.display_item(self, result) output.replace_item_with_key(self, result) - super(CallbackModule, self).v2_playbook_item_on_ok(result) + super(CallbackModule, self).v2_runner_item_on_ok(result) - def v2_playbook_item_on_failed(self, 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_playbook_item_on_failed(result) + super(CallbackModule, self).v2_runner_item_on_failed(result) - def v2_playbook_item_on_skipped(self, 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_playbook_item_on_skipped(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 new file mode 100644 index 0000000000..c0742ca2a3 --- /dev/null +++ b/lib/trellis/plugins/callback/vars.py @@ -0,0 +1,104 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re +import sys + +from __main__ import cli +from ansible.module_utils.six import iteritems +from ansible.errors import AnsibleError +from ansible.parsing.yaml.objects import AnsibleMapping, AnsibleSequence, AnsibleUnicode +from ansible.playbook.play_context import PlayContext +from ansible.playbook.task import Task +from ansible.plugins.callback import CallbackBase +from ansible.template import Templar + + +class CallbackModule(CallbackBase): + ''' Creates and modifies play and host variables ''' + + CALLBACK_VERSION = 2.0 + CALLBACK_NAME = 'vars' + + def __init__(self): + self._options = cli.options if cli else None + + 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())) + + # process list values + elif isinstance(item, AnsibleSequence): + return AnsibleSequence([self.raw_triage('.'.join([key_string, str(i)]), value, patterns) for i,value in enumerate(item)]) + + # 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 + + def raw_vars(self, play, host, hostvars): + if 'raw_vars' not in hostvars: + return + + 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) + for key in keys: + if key in play.vars: + play.vars[key] = self.raw_triage(key, play.vars[key], patterns) + elif key in hostvars: + host.vars[key] = self.raw_triage(key, hostvars[key], patterns) + + def cli_options(self): + options = [] + + strings = { + '--connection': 'connection', + '--private-key': 'private_key_file', + '--ssh-common-args': 'ssh_common_args', + '--ssh-extra-args': 'ssh_extra_args', + '--timeout': 'timeout', + '--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 inventory in getattr(self._options, 'inventory'): + options.append("--inventory='{}'".format(str(inventory))) + + if getattr(self._options, '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 + + try: + import passlib.hash + return False + except: + return True + + def v2_playbook_on_play_start(self, play): + env = play.get_variable_manager().get_vars(play=play).get('env', '') + env_group = next((group for key,group in play.get_variable_manager()._inventory.groups.iteritems() if key == env), False) + if env_group: + env_group.set_priority(20) + + for host in play.get_variable_manager()._inventory.list_hosts(play.hosts[0]): + # it should be ok to remove dummy Task() once minimum required Ansible >= 2.4.2 + hostvars = play.get_variable_manager().get_vars(play=play, host=host, task=Task()) + 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() diff --git a/lib/trellis/plugins/filter/filters.py b/lib/trellis/plugins/filter/filters.py index b76497c453..6c8eed5eb7 100644 --- a/lib/trellis/plugins/filter/filters.py +++ b/lib/trellis/plugins/filter/filters.py @@ -1,45 +1,14 @@ # Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) +from __future__ import (absolute_import, division, print_function, unicode_literals) __metaclass__ = type import types from ansible import errors -from ansible.compat.six import string_types - -def reverse_www(hosts, enabled=True, append=True): - ''' Add or remove www subdomain ''' - - if not enabled: - return hosts - - # Check if hosts is a list and parse each host - if isinstance(hosts, (list, tuple, types.GeneratorType)): - reversed_hosts = [reverse_www(host) for host in hosts] - - if append: - return list(set(hosts + reversed_hosts)) - else: - return reversed_hosts - - # Add or remove www - elif isinstance(hosts, string_types): - host = hosts - - if host.startswith('www.'): - return host[4:] - else: - if len(host.split('.')) > 2: - return host - else: - return 'www.{0}'.format(host) - - # Handle invalid input type - else: - raise errors.AnsibleFilterError('The reverse_www filter expects a string or list of strings, got ' + repr(hosts)) +from ansible.module_utils.six import string_types def to_env(dict_value): - envs = ["{0}='{1}'".format(key.upper(), value) 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): @@ -51,7 +20,6 @@ class FilterModule(object): def filters(self): return { - 'reverse_www': reverse_www, 'to_env': to_env, 'underscore': underscore, } diff --git a/lib/trellis/plugins/vars/vars.py b/lib/trellis/plugins/vars/vars.py deleted file mode 100644 index 6ebfdef178..0000000000 --- a/lib/trellis/plugins/vars/vars.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible import __version__ -from ansible.errors import AnsibleError - -if __version__.startswith('1'): - raise AnsibleError('Trellis no longer supports Ansible 1.x. Please upgrade to Ansible 2.x.') - -# These imports will produce Traceback in Ansible 1.x, so place after version check -from __main__ import cli -from ansible.compat.six import iteritems - - -class VarsModule(object): - ''' Creates and modifies host variables ''' - - def __init__(self, inventory): - self.inventory = inventory - self.inventory_basedir = inventory.basedir() - self._options = cli.options if cli else None - - # Wrap salts and keys variables in {% raw %} to prevent jinja templating errors - def wrap_salts_in_raw(self, host, hostvars): - if 'vault_wordpress_sites' in hostvars: - for name, site in hostvars['vault_wordpress_sites'].iteritems(): - for key, value in site['env'].iteritems(): - if key.endswith(('_key', '_salt')) and not value.startswith(('{% raw', '{%raw')): - hostvars['vault_wordpress_sites'][name]['env'][key] = ''.join(['{% raw %}', value, '{% endraw %}']) - host.vars['vault_wordpress_sites'] = hostvars['vault_wordpress_sites'] - - def cli_options_ping(self): - options = [] - - strings = { - '--connection': 'connection', - '--inventory-file': 'inventory', - '--private-key': 'private_key_file', - '--ssh-common-args': 'ssh_common_args', - '--ssh-extra-args': 'ssh_extra_args', - '--timeout': 'timeout', - '--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)))) - - booleans = { - '--ask-pass': 'ask_pass', - '--ask-vault-pass': 'ask_vault_pass', - } - - for option,value in booleans.iteritems(): - if getattr(self._options, value, False): - options.append(option) - - return ' '.join(options) - - def get_host_vars(self, host, vault_password=None): - self.wrap_salts_in_raw(host, host.get_group_vars()) - host.vars['cli_options_ping'] = self.cli_options_ping() - return {} diff --git a/lib/trellis/plugins/vars/version.py b/lib/trellis/plugins/vars/version.py new file mode 100644 index 0000000000..273ae5f310 --- /dev/null +++ b/lib/trellis/plugins/vars/version.py @@ -0,0 +1,35 @@ +# 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 + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + +version_requirement = '2.4.0.0' +version_tested_max = '2.4.2.0' + +if not ge(LooseVersion(__version__), LooseVersion(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 ' + 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)) + +# Import BaseVarsPlugin after Ansible version check. +# Otherwise import error for Ansible versions older than 2.4 would prevent display of version check message. +from ansible.plugins.vars import BaseVarsPlugin + + +class VarsModule(BaseVarsPlugin): + + def get_vars(self, loader, path, entities, cache=True): + return {} diff --git a/lib/trellis/utils/output.py b/lib/trellis/utils/output.py index 63fdee8f06..7d45369e76 100644 --- a/lib/trellis/utils/output.py +++ b/lib/trellis/utils/output.py @@ -8,7 +8,7 @@ import textwrap from ansible import __version__ -from ansible.utils.unicode import to_unicode +from ansible.module_utils._text import to_text def system(vagrant_version=None): # Get most recent Trellis CHANGELOG entry @@ -27,9 +27,9 @@ def system(vagrant_version=None): # Retrieve most recent changelog entry else: - change = re.search(r'.*\n\*\s*([^\(\n\[]+)', str) + 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(1).strip()) + 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 '' @@ -42,15 +42,19 @@ def reset_task_info(obj, task=None): obj.first_host = True obj.first_item = True obj.task_failed = False - obj.vagrant_version = None # Display dict key only, instead of full json dump def replace_item_with_key(obj, result): - if not obj._display.verbosity: - if 'key' in result._result['item']: - result._result['item'] = result._result['item']['key'] - elif 'item' in result._result['item'] and 'key' in result._result['item']['item']: - result._result['item'] = result._result['item']['item']['key'] + 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 = '' @@ -58,10 +62,9 @@ def display(obj, result): display = obj._display.display wrap_width = 77 first = obj.first_host and obj.first_item - failed = 'failed' in result or 'unreachable' in result # 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'): + if 'msg' in result and (obj.task_failed or obj.action == 'debug'): msg = result.pop('msg', '') # Disable Ansible's verbose setting for debug module to avoid the CallbackBase._dump_results() @@ -69,8 +72,8 @@ def display(obj, 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_unicode(result[item]) != '') + if obj.task_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, '')]) @@ -79,9 +82,9 @@ def display(obj, result): # Must pass unicode strings to Display.display() to prevent UnicodeError tracebacks if isinstance(msg, list): - msg = '\n'.join([to_unicode(x) for x in msg]) + msg = '\n'.join([to_text(x) for x in msg]) elif not isinstance(msg, unicode): - msg = to_unicode(msg) + msg = to_text(msg) # Wrap text msg = '\n'.join([textwrap.fill(line, wrap_width, replace_whitespace=False) @@ -102,7 +105,7 @@ def display(obj, result): else: if not first: display(hr, 'bright gray') - display(msg, 'red' if failed else 'bright purple') + display(msg, 'red' if obj.task_failed else 'bright purple') def display_host(obj, result): if 'results' not in result._result: diff --git a/lib/trellis/vagrant.rb b/lib/trellis/vagrant.rb new file mode 100644 index 0000000000..16cf2f3dd4 --- /dev/null +++ b/lib/trellis/vagrant.rb @@ -0,0 +1,78 @@ +# 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/requirements.yml b/requirements.yml index d9cbefb791..b000c5c566 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,10 +1,10 @@ - name: composer src: geerlingguy.composer - version: 1.5.0 + version: 1.6.1 - name: ntp - src: resmo.ntp - version: 0.3.0 + src: geerlingguy.ntp + version: 1.5.2 - name: logrotate src: nickhammond.logrotate @@ -12,14 +12,11 @@ - name: swapfile src: kamaln7.swapfile - version: 0.4 - -- src: geerlingguy.daemonize - version: 1.1.0 + version: 4850d8a - name: mailhog src: geerlingguy.mailhog - version: 1.0.5 + version: 2.1.3 - name: ansible src: geerlingguy.ansible diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index 2f60766019..5e92f1487f 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -1,2 +1,41 @@ -minimum_ansible_version: 2.0.2.0 -default_timezone: Etc/UTC +ntp_timezone: Etc/UTC + +env_groups: "{{ ['development', 'staging', 'production'] | intersect(group_names) }}" + +envs_with_wp_sites: "{{ + lookup('filetree', playbook_dir + '/group_vars') | + selectattr('path', 'match', '(' + env_groups | join('|') + ')/wordpress_sites\\.yml$') | + map(attribute='path') | map('regex_replace', '([^/]*)/.*', '\\1') | list +}}" + +site_keys_by_env_pair: "[ + {% for env_pair in envs_with_wp_sites | combinations(2) | list %} + { + 'env_pair': {{ env_pair }}, + 'site_keys': {{ + (vars[env_pair[0] + '_sites'].wordpress_sites | default({})).keys() | intersect( + (vars[env_pair[1] + '_sites'].wordpress_sites | default({})).keys()) + }} + }, + {% endfor %} +]" + +apt_packages_default: + python-software-properties: "{{ apt_package_state }}" + python-pycurl: "{{ apt_package_state }}" + build-essential: "{{ apt_package_state }}" + python-mysqldb: "{{ apt_package_state }}" + curl: "{{ apt_package_state }}" + git-core: "{{ apt_package_state }}" + dbus: "{{ apt_package_state }}" + libnss-myhostname: "{{ apt_package_state }}" + +apt_packages_custom: {} +apt_packages: "{{ apt_packages_default | combine(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 %}" diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 4f1e2add53..cab7d53088 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -1,4 +1,7 @@ --- +- name: disable temporary challenge sites + import_tasks: disable_challenge_sites.yml + - name: restart memcached service: name: memcached @@ -6,8 +9,7 @@ - name: reload php-fpm service: - name: php7.0-fpm + name: php7.1-fpm state: reloaded -- name: reload nginx - include: reload_nginx.yml +- import_tasks: reload_nginx.yml diff --git a/roles/common/tasks/disable_challenge_sites.yml b/roles/common/tasks/disable_challenge_sites.yml new file mode 100644 index 0000000000..455d7f0cad --- /dev/null +++ b/roles/common/tasks/disable_challenge_sites.yml @@ -0,0 +1,7 @@ +--- +- name: disable temporary challenge sites + file: + path: "{{ nginx_path }}/sites-enabled/letsencrypt-{{ item }}.conf" + state: absent + with_items: "{{ wordpress_sites.keys() }}" + notify: reload nginx diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 670d7f9d8e..67929410d9 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -1,52 +1,154 @@ --- -- name: Validate Ansible version - assert: - that: - - "{{ ansible_version is defined }}" - - "{{ ansible_version.full | version_compare(minimum_ansible_version, '>=') }}" - msg: "Your Ansible version is too old. Trellis requires at least {{ minimum_ansible_version }}. Your version is {{ ansible_version.full | default('< 1.6') }}" +- block: + - name: Load wordpress_sites.yml vars into _sites vars + include_vars: + file: group_vars/{{ item }}/wordpress_sites.yml + name: "{{ item }}_sites" + with_items: "{{ envs_with_wp_sites }}" + when: envs_with_wp_sites | count > 1 + + - name: Fail if there are duplicate site keys within host's wordpress_sites + fail: + msg: > + If you put multiple environments on `{{ inventory_hostname }}`, `wordpress_sites` + must use different site keys per environment. Adjust the following site keys that + are duplicated between the `{{ item.env_pair | join('` and `') }}` groups: + {{ item.site_keys | to_nice_yaml | indent(2) }} + when: item.site_keys | count + with_items: "{{ site_keys_by_env_pair }}" + + when: + - env_groups | count > 1 + - validate_site_keys | default(true) | bool + +- name: Validate wordpress_sites + fail: + msg: "{{ lookup('template', 'wordpress_sites.j2') }}" + when: wordpress_sites.keys() | difference(vault_wordpress_sites.keys()) | count + 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 + tags: [letsencrypt, wordpress] + +- name: Verify dict format for apt package component variables + fail: + msg: "{{ lookup('template', 'package_vars_wrong_format_msg.j2') }}" + when: package_vars_wrong_format | count + 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: "[{% for k,v in package_vars.iteritems() if v | type_debug != 'dict' %}'{{ k }}',{% endfor %}]" + tags: [sshd, memcached, php] + +- name: Verify dict format for apt package combined variables + fail: + msg: "{{ lookup('template', 'package_vars_wrong_format_msg.j2') }}" + when: package_vars_wrong_format | count + vars: + package_vars: + apt_packages: "{{ apt_packages }}" + memcached_packages: "{{ memcached_packages }}" + php_extensions: "{{ php_extensions }}" + sshd_packages: "{{ sshd_packages }}" + package_vars_wrong_format: "[{% for k,v in package_vars.iteritems() if v | type_debug != 'dict' %}'{{ k }}',{% endfor %}]" + tags: [sshd, memcached, php] + +- name: Validate Ubuntu version + debug: + msg: | + Trellis is built for Ubuntu 16.04 Xenial as of https://github.com/roots/trellis/pull/626 + + Your Ubuntu version is {{ ansible_distribution_version }} {{ ansible_distribution_release }} + + 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: Update Apt - apt: - update_cache: yes +- name: Check whether passlib is needed + 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] - name: Checking essentials apt: - name: "{{ item }}" - state: present - with_items: - - python-software-properties - - python-pycurl - - build-essential - - python-mysqldb - - curl - - git-core - - zip - - unzip - - imagemagick - - htop - - dbus - - acl + name: "{{ item.key }}" + state: "{{ item.value }}" + cache_valid_time: "{{ apt_cache_valid_time }}" + with_dict: "{{ apt_packages }}" - name: Validate timezone variable stat: - path: /usr/share/zoneinfo/{{ default_timezone }} + path: /usr/share/zoneinfo/{{ ntp_timezone }} register: timezone_path changed_when: false - name: Explain timezone error fail: - msg: "{{ default_timezone }} is not a valid timezone. For a list of valid timezones, check https://php.net/manual/en/timezones.php" + 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 -- name: Get current timezone - command: cat /etc/timezone - register: current_timezone - changed_when: false +- name: Add myhostname to nsswitch.conf to ensure resolvable hostname + lineinfile: + backrefs: yes + backup: yes + dest: /etc/nsswitch.conf + line: \1 myhostname + regexp: ^(hosts\:((?!myhostname).)*)$ + state: present + +- name: Generate SSH key for vagrant user + user: + name: vagrant + generate_ssh_key: yes + when: env == 'development' -- name: Set timezone - command: timedatectl set-timezone {{ default_timezone }} - when: current_timezone.stdout != default_timezone +- name: Retrieve SSH client IP + ipify_facts: + connection: local + become: no + when: env != 'development' and ssh_client_ip_lookup | default(true) + tags: [fail2ban, ferm] - include: symlinks.yml diff --git a/roles/common/tasks/reload_nginx.yml b/roles/common/tasks/reload_nginx.yml index c0af12e9c1..952a6082e0 100644 --- a/roles/common/tasks/reload_nginx.yml +++ b/roles/common/tasks/reload_nginx.yml @@ -1,8 +1,7 @@ --- - name: reload nginx command: nginx -t - register: nginx_test - notify: "{{ (ansible_version.full | version_compare('2.1.1.0', '>=') and role_path | basename == 'common') | ternary('perform nginx reload', omit) }}" + notify: "{{ (role_path | basename == 'common') | ternary('perform nginx reload', omit) }}" - name: perform nginx reload service: 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/site_hosts.j2 b/roles/common/templates/site_hosts.j2 new file mode 100644 index 0000000000..6ad7aa34a1 --- /dev/null +++ b/roles/common/templates/site_hosts.j2 @@ -0,0 +1,17 @@ +Required format for `site_hosts` (group_vars/{{ env }}/wordpress_sites.yml): + +example.com: + site_hosts: + - canonical: example.com + +The above is the minimum required. Multiple hosts and redirects are possible: + +example.com: + site_hosts: + - canonical: example.com + redirects: + - www.example.com + - site.com + - canonical: example.co.uk + redirects: + - www.example.co.uk diff --git a/roles/common/templates/validate_ssh_msg.j2 b/roles/common/templates/validate_ssh_msg.j2 new file mode 100644 index 0000000000..6d49d95aef --- /dev/null +++ b/roles/common/templates/validate_ssh_msg.j2 @@ -0,0 +1,32 @@ +{% 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/common/templates/wordpress_sites.j2 b/roles/common/templates/wordpress_sites.j2 new file mode 100644 index 0000000000..07e4136cb6 --- /dev/null +++ b/roles/common/templates/wordpress_sites.j2 @@ -0,0 +1,10 @@ +Invalid WordPress sites configuration: site names in `wordpress_sites` must have matching entry in `vault_wordpress_sites`. + +Sites without a matching vault entry: +{% for name in wordpress_sites.keys() | difference(vault_wordpress_sites.keys()) %} +* `{{ name }}` +{% endfor %} + +Update `group_vars/{{ env }}/vault.yml` to continue. + +Docs: https://roots.io/trellis/docs/wordpress-sites/#passwordssecrets diff --git a/roles/connection/defaults/main.yml b/roles/connection/defaults/main.yml new file mode 100644 index 0000000000..f6c42a8444 --- /dev/null +++ b/roles/connection/defaults/main.yml @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000000..92ba31315d --- /dev/null +++ b/roles/connection/tasks/main.yml @@ -0,0 +1,83 @@ +--- +- name: Require manual definition of remote-user + fail: + msg: | + When using `--ask-pass` option, use `-u` option to define remote-user: + 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 + -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('') }}" + failed_when: false + changed_when: false + check_mode: no + register: connection_status + tags: [connection-tests] + +- name: Warn about change in host keys + fail: + msg: | + WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! + + If this change in host keys is expected (e.g., if you rebuilt the server + or if the Trellis sshd role made changes recently), then run the following + command to clear the old host key from your known_hosts. + + ssh-keygen -R {{ connection_status.stdout | regex_replace('(.|\n)*host key for (.*) has changed(.|\n)*', '\2') }} + + Then try your Trellis playbook or SSH connection again. + + If the change is unexpected, cautiously consider why the host identification + may have changed and whether you may be victim to a man-in-the-middle attack. + + --------------------------------------------------- + {{ (connection_status.stdout.replace('Please contact your system administrator.\r\n', '') | + regex_replace ('(.|\n)*(The fingerprint for the(.|\n)*Host key verification failed.)(.|\n)*', '\2') | + regex_replace('(\\r\\n|\\n)', '\n\n')).replace('\"', '"') }} + when: "'REMOTE HOST IDENTIFICATION HAS CHANGED' in connection_status.stdout" + tags: [connection-tests] + +- 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)) }}" + 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: + ansible_become_pass: "{% raw %}{% for user in vault_users | default([]) if user.name == ansible_user %}{{ user.password | default('') }}{% endfor %}{% endraw %}" + when: ansible_user != 'root' and not cli_ask_become_pass | default(false) and ansible_become_pass is not defined + no_log: true + + when: dynamic_user | default(true) diff --git a/roles/fail2ban/README.md b/roles/fail2ban/README.md index b810d7201e..a3aa07b68e 100644 --- a/roles/fail2ban/README.md +++ b/roles/fail2ban/README.md @@ -12,8 +12,8 @@ Below is a list of default values along with a description of what they do. ``` # Which log level should it be output as? -# 1 = ERROR, 2 = WARN, 3 = INFO, 4 = DEBUG -fail2ban_loglevel: 3 +# Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG. Default: ERROR +fail2ban_loglevel: WARNING # Where should log outputs be sent to? # SYSLOG, STDERR, STDOUT, file diff --git a/roles/fail2ban/defaults/main.yml b/roles/fail2ban/defaults/main.yml index b3a2b6f2ad..473ae8a0d5 100644 --- a/roles/fail2ban/defaults/main.yml +++ b/roles/fail2ban/defaults/main.yml @@ -1,5 +1,7 @@ --- -fail2ban_loglevel: 3 +fail2ban_package: fail2ban + +fail2ban_loglevel: INFO fail2ban_logtarget: /var/log/fail2ban.log fail2ban_socket: /var/run/fail2ban/fail2ban.sock diff --git a/roles/fail2ban/tasks/main.yml b/roles/fail2ban/tasks/main.yml index 3af663a73e..dcc6adc5ab 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 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/tasks/main.yml b/roles/ferm/tasks/main.yml index b5d691868b..63b0b0a4d0 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: diff --git a/roles/letsencrypt/defaults/main.yml b/roles/letsencrypt/defaults/main.yml index 900e70caf5..2bd7b12805 100644 --- a/roles/letsencrypt/defaults/main.yml +++ b/roles/letsencrypt/defaults/main.yml @@ -1,10 +1,10 @@ sites_using_letsencrypt: "[{% for name, site in wordpress_sites.iteritems() if site.ssl.enabled and site.ssl.provider | default('manual') == 'letsencrypt' %}'{{ name }}',{% endfor %}]" -letsencrypt_enabled: "{{ sites_using_letsencrypt | count > 0 }}" -site_uses_letsencrypt: "{{ item.value.ssl is defined and item.value.ssl.enabled | default(false) and item.value.ssl.provider | default('manual') == 'letsencrypt' }}" -sites_need_confs: "False in [{% for item in nginx_confs.results if 'stat' in item %}{{ item.stat.exists }},{% endfor %}]" +site_uses_letsencrypt: ssl_enabled and item.value.ssl.provider | default('manual') == 'letsencrypt' +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 %} }" acme_tiny_repo: 'https://github.com/diafygi/acme-tiny.git' -acme_tiny_commit: '69a457269a6392ac31b629b4e103e8ea7dd282c9' +acme_tiny_commit: '4ed13950c0a9cf61f1ca81ff1874cde1cf48ab32' acme_tiny_software_directory: /usr/local/letsencrypt acme_tiny_data_directory: /var/lib/letsencrypt diff --git a/roles/letsencrypt/tasks/certificates.yml b/roles/letsencrypt/tasks/certificates.yml index b74edbcb05..74dd7e5997 100644 --- a/roles/letsencrypt/tasks/certificates.yml +++ b/roles/letsencrypt/tasks/certificates.yml @@ -1,10 +1,10 @@ +--- - name: Generate private keys shell: openssl genrsa 4096 > {{ letsencrypt_keys_dir }}/{{ item.key }}.key args: creates: "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" when: site_uses_letsencrypt with_dict: "{{ wordpress_sites }}" - tags: [letsencrypt_keys] - name: Ensure correct permissions on private keys file: @@ -12,37 +12,36 @@ mode: 0600 when: site_uses_letsencrypt with_dict: "{{ wordpress_sites }}" - tags: [letsencrypt_keys] -- name: Generate CSRs for single domain keys - shell: openssl req -new -sha256 -key "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" -subj "/CN={{ item.value.site_hosts[0] }}" > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr - args: - creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" - when: site_uses_letsencrypt and item.value.site_hosts | length == 1 and not item.value.www_redirect | default(true) +- name: Generate Lets Encrypt certificate IDs + shell: | + echo "{{ [site_hosts | join(' '), letsencrypt_ca, acme_tiny_commit, letsencrypt_intermediate_cert_sha256sum] | join('\n') }}" | + cat {{ letsencrypt_account_key }} {{ letsencrypt_keys_dir }}/{{ item.key }}.key - | + md5sum | cut -c -7 + register: generate_cert_ids + changed_when: false + when: site_uses_letsencrypt with_dict: "{{ wordpress_sites }}" - tags: [letsencrypt_keys] + tags: [wordpress, wordpress-setup, nginx-includes] -- name: Generate CSRs for multiple domain keys - 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:{{ item.value.site_hosts | reverse_www(enabled=item.value.www_redirect | default(true)) | join(',DNS:') }}')) > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" +- 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" args: executable: /bin/bash - creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" - when: site_uses_letsencrypt and item.value.www_redirect | default(true) + creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}-{{ letsencrypt_cert_ids[item.key] }}.csr" + when: site_uses_letsencrypt with_dict: "{{ wordpress_sites }}" - tags: [letsencrypt_keys] -- name: Generate the initial certificate +- name: Generate certificate renewal script + template: + src: renew-certs.py + dest: "{{ acme_tiny_data_directory }}/renew-certs.py" + mode: 0700 + +- name: Generate the certificates command: ./renew-certs.py args: chdir: "{{ acme_tiny_data_directory }}" - register: generate_initial_cert - changed_when: generate_initial_cert.stdout is defined and 'Created' in generate_initial_cert.stdout - notify: reload nginx - -- name: Disable Nginx site - file: - path: "{{ nginx_path }}/sites-enabled/letsencrypt-{{ item.key }}.conf" - state: absent - with_dict: "{{ wordpress_sites }}" - when: sites_need_confs + register: generate_certs + changed_when: generate_certs.stdout is defined and 'Created' in generate_certs.stdout notify: reload nginx diff --git a/roles/letsencrypt/tasks/main.yml b/roles/letsencrypt/tasks/main.yml index 0abf592760..27c4b86ac7 100644 --- a/roles/letsencrypt/tasks/main.yml +++ b/roles/letsencrypt/tasks/main.yml @@ -1,13 +1,14 @@ -- include: setup.yml -- include: nginx.yml -- include: certificates.yml +--- +- import_tasks: setup.yml +- import_tasks: nginx.yml +- import_tasks: certificates.yml - name: Install cronjob for key generation cron: cron_file: letsencrypt-certificate-renewal name: letsencrypt certificate renewal user: root - job: cd {{ acme_tiny_data_directory }} && ./renew-certs.py + job: cd {{ acme_tiny_data_directory }} && ./renew-certs.py && /usr/sbin/service nginx reload day: "{{ letsencrypt_cronjob_daysofmonth }}" hour: 4 minute: 30 diff --git a/roles/letsencrypt/tasks/nginx.yml b/roles/letsencrypt/tasks/nginx.yml index 564cdf8488..298524cc07 100644 --- a/roles/letsencrypt/tasks/nginx.yml +++ b/roles/letsencrypt/tasks/nginx.yml @@ -1,33 +1,42 @@ -- name: Check for existing Nginx conf per site - stat: - path: "{{ nginx_path }}/sites-enabled/{{ item.key }}.conf" - register: nginx_confs - when: site_uses_letsencrypt - with_dict: "{{ wordpress_sites }}" - +--- - name: Create Nginx conf for challenges location template: src: acme-challenge-location.conf.j2 dest: "{{ nginx_path }}/acme-challenge-location.conf" - when: sites_need_confs + +- name: Get list of hosts in current Nginx conf + shell: | + [ ! -f {{ nginx_path }}/sites-enabled/{{ item.key }}.conf ] || + sed -n -e "/listen 80/,/server_name/{s/server_name \(.*\);/\1/p}" {{ nginx_path }}/sites-enabled/{{ item.key }}.conf + register: current_hosts + changed_when: false + when: site_uses_letsencrypt + with_dict: "{{ wordpress_sites }}" - name: Create needed Nginx confs for challenges template: src: nginx-challenge-site.conf.j2 - dest: "{{ nginx_path }}/sites-available/letsencrypt-{{ item.item.key }}.conf" - when: not item | skipped and not item.stat.exists - with_items: "{{ nginx_confs.results }}" + dest: "{{ nginx_path }}/sites-available/letsencrypt-{{ item.key }}.conf" + register: challenge_site_confs + when: + - site_uses_letsencrypt + - missing_hosts | count + with_dict: "{{ wordpress_sites }}" - name: Enable Nginx sites file: - src: "{{ nginx_path }}/sites-available/letsencrypt-{{ item.item.key }}.conf" - dest: "{{ nginx_path }}/sites-enabled/letsencrypt-{{ item.item.key }}.conf" + src: "{{ nginx_path }}/sites-available/letsencrypt-{{ item.key }}.conf" + dest: "{{ nginx_path }}/sites-enabled/letsencrypt-{{ item.key }}.conf" state: link - when: not item | skipped and not item.stat.exists - with_items: "{{ nginx_confs.results }}" + register: challenge_sites_enabled + when: + - site_uses_letsencrypt + - missing_hosts | count + with_dict: "{{ wordpress_sites }}" + notify: disable temporary challenge sites -- include: "{{ playbook_dir }}/roles/common/tasks/reload_nginx.yml" - when: sites_need_confs +- import_tasks: "{{ playbook_dir }}/roles/common/tasks/reload_nginx.yml" + when: challenge_site_confs | changed or challenge_sites_enabled | changed - name: Create test Acme Challenge file shell: touch {{ acme_tiny_challenges_directory }}/ping.txt @@ -37,7 +46,7 @@ - name: Test Acme Challenges test_challenges: - hosts: "{{ item.value.site_hosts | reverse_www(enabled=item.value.www_redirect | default(true)) }}" + hosts: "{{ site_hosts }}" register: letsencrypt_test_challenges ignore_errors: true when: site_uses_letsencrypt diff --git a/roles/letsencrypt/tasks/setup.yml b/roles/letsencrypt/tasks/setup.yml index 49b0886bbd..6c0d6afcc7 100644 --- a/roles/letsencrypt/tasks/setup.yml +++ b/roles/letsencrypt/tasks/setup.yml @@ -1,3 +1,4 @@ +--- - name: Create directories and set permissions file: mode: "{{ item.mode | default(omit) }}" @@ -35,15 +36,8 @@ shell: openssl genrsa 4096 > {{ letsencrypt_account_key }} args: creates: "{{ letsencrypt_account_key }}" - register: generate_account_key when: letsencrypt_account_key_source_content is not defined and letsencrypt_account_key_source_file is not defined -- name: Generate certificate renewal script - template: - src: renew-certs.py - dest: "{{ acme_tiny_data_directory }}/renew-certs.py" - mode: 0700 - - name: Download intermediate certificate get_url: url: "{{ letsencrypt_intermediate_cert_url }}" diff --git a/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 b/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 index 7b80c271ce..2741378f79 100644 --- a/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 +++ b/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 @@ -1,5 +1,5 @@ server { listen 80; - server_name {{ item.item.value.site_hosts | reverse_www(enabled=item.item.value.www_redirect | default(true)) | join(' ') }}; + 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 06a01b61fb..88cd133419 100644 --- a/roles/letsencrypt/templates/renew-certs.py +++ b/roles/letsencrypt/templates/renew-certs.py @@ -6,14 +6,12 @@ from subprocess import CalledProcessError, check_output, STDOUT -certs_dir = '{{ letsencrypt_certs_dir }}' failed = False -sites = {{ wordpress_sites }} -sites = (k for k, v in sites.items() if 'ssl' in v and v['ssl'].get('enabled', False) and v['ssl'].get('provider', 'manual') == 'letsencrypt') +letsencrypt_cert_ids = {{ letsencrypt_cert_ids }} -for site in sites: - cert_path = os.path.join(certs_dir, site + '.cert') - bundled_cert_path = os.path.join(certs_dir, site + '-bundled.cert') +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') if os.access(cert_path, os.F_OK): stat = os.stat(cert_path) @@ -26,11 +24,12 @@ print 'Generating certificate for ' + site 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}.csr ' + '--csr {{ acme_tiny_data_directory }}/csrs/{0}-{1}.csr ' '--acme-dir {{ acme_tiny_challenges_directory }}' - ).format(site) + ).format(site, letsencrypt_cert_ids[site]) try: cert = check_output(cmd, stderr=STDOUT, shell=True) diff --git a/roles/mariadb/defaults/main.yml b/roles/mariadb/defaults/main.yml index 796065a35e..1a92bafb1b 100644 --- a/roles/mariadb/defaults/main.yml +++ b/roles/mariadb/defaults/main.yml @@ -1,8 +1,11 @@ -mariadb_binary_logging_disabled: true -mariadb_keyserver_fingerprint: "0xcbcb082a1bb943db" -mariadb_mirror: nyc2.mirrors.digitalocean.com -mariadb_version: "10.0" -mariadb_dist: trusty +mariadb_keyserver: keyserver.ubuntu.com +mariadb_keyserver_id: "0xF1656F24C74CD1D8" +mariadb_ppa: "deb [arch=amd64,i386,ppc64el] http://ftp.osuosl.org/pub/mariadb/repo/10.2/ubuntu xenial 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 %}]" diff --git a/roles/mariadb/handlers/main.yml b/roles/mariadb/handlers/main.yml new file mode 100644 index 0000000000..3bde7e4292 --- /dev/null +++ b/roles/mariadb/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart mysql server + service: + name: mysql + state: restarted + enabled: true diff --git a/roles/mariadb/tasks/main.yml b/roles/mariadb/tasks/main.yml index 9e69f8b592..0aa0951b61 100644 --- a/roles/mariadb/tasks/main.yml +++ b/roles/mariadb/tasks/main.yml @@ -1,27 +1,27 @@ --- -- name: Add MariaDB MySQL apt-key - apt_key: - url: "http://keyserver.ubuntu.com/pks/lookup?op=get&fingerprint=on&search={{ mariadb_keyserver_fingerprint }}" - state: present +- block: + - name: Add MariaDB APT key + apt_key: + keyserver: "{{ mariadb_keyserver }}" + id: "{{ mariadb_keyserver_id }}" -- name: Add MariaDB MySQL deb and deb-src - apt_repository: - repo: "{{ item }}" - state: present - with_items: - - "deb http://{{ mariadb_mirror }}/mariadb/repo/{{ mariadb_version }}/ubuntu {{ mariadb_dist | default(ansible_distribution_release) }} main" - - "deb-src http://{{ mariadb_mirror }}/mariadb/repo/{{ mariadb_version }}/ubuntu {{ mariadb_dist | default(ansible_distribution_release) }} main" + - name: Add MariaDB PPA + apt_repository: + repo: "{{ mariadb_ppa }}" + update_cache: yes - name: Install MySQL client apt: - name: mariadb-client - state: present + name: "{{ mariadb_client_package }}" + state: "{{ mariadb_client_package_state | default(apt_package_state) }}" + cache_valid_time: "{{ apt_cache_valid_time }}" - block: - - name: Install MariaDB MySQL server + - name: Install MySQL server apt: - name: mariadb-server - state: present + 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: @@ -29,13 +29,8 @@ dest: /etc/mysql/conf.d owner: root group: root - when: mariadb_binary_logging_disabled - - - name: Restart MariaDB MySQL Server - service: - name: mysql - state: restarted - enabled: true + when: mysql_binary_logging_disabled + notify: restart mysql server - name: Set root user password mysql_user: diff --git a/roles/nginx/defaults/main.yml b/roles/nginx/defaults/main.yml index dbfdacd430..f70ca149a8 100644 --- a/roles/nginx/defaults/main.yml +++ b/roles/nginx/defaults/main.yml @@ -1,21 +1,18 @@ --- +nginx_ppa: "ppa:nginx/development" +nginx_package: nginx +nginx_conf: nginx.conf.j2 nginx_path: /etc/nginx nginx_logs_root: /var/log/nginx -nginx_user: www-data +nginx_user: www-data www-data nginx_fastcgi_buffers: 8 8k -nginx_fastcgi_buffer_size: 4k -nginx_ssl_path: "{{ nginx_path }}/ssl" - -# HSTS defaults -nginx_hsts_max_age: 31536000 -nginx_hsts_include_subdomains: true -nginx_hsts_preload: true +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 -nginx_cache_duration: 30s nginx_cache_key_storage_size: 10m nginx_cache_size: 250m nginx_cache_inactive: 1h -nginx_skip_cache_uri: /wp-admin/|/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 diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml index 8790c51f67..20e77435fa 100644 --- a/roles/nginx/tasks/main.yml +++ b/roles/nginx/tasks/main.yml @@ -1,14 +1,14 @@ --- - name: Add Nginx PPA apt_repository: - repo: "ppa:nginx/development" + repo: "{{ nginx_ppa }}" update_cache: yes - name: Install Nginx apt: - name: nginx - state: present - force: yes + name: "{{ nginx_package }}" + state: "{{ nginx_package_state | default(apt_package_state) }}" + cache_valid_time: "{{ apt_cache_valid_time }}" - name: Create SSL directory file: @@ -21,6 +21,7 @@ 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] @@ -28,19 +29,21 @@ git: repo: "https://github.com/h5bp/server-configs-nginx.git" dest: "{{ nginx_path }}/h5bp-server-configs" - version: 82181a672a7c26f9bc8744fead80318d8a2520b1 + version: c5c6602232e0976d9e69d69874aa84d2a2698265 force: yes - name: Move h5bp configs - command: cp -R {{ nginx_path }}/h5bp-server-configs/h5bp {{ nginx_path }}/h5bp - args: - creates: "{{ nginx_path }}/h5bp/" + 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 != '' + notify: reload nginx - name: Create nginx.conf template: - src: nginx.conf.j2 + src: "{{ nginx_conf }}" dest: "{{ nginx_path }}/nginx.conf" notify: reload nginx + tags: nginx-includes - include: cloudflare.yml when: env != 'development' @@ -51,17 +54,16 @@ state: absent notify: reload nginx -- name: Enable better default site to drop unknown requests - command: cp {{ nginx_path }}/h5bp-server-configs/sites-available/no-default {{ nginx_path }}/sites-enabled/no-default.conf - args: - creates: "{{ nginx_path }}/sites-enabled/no-default.conf" - notify: reload nginx - -- name: Create base WordPress config +- name: Create Nginx available sites template: - src: wordpress.conf.j2 - dest: "{{ nginx_path }}/wordpress.conf" + 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 +<<<<<<< HEAD - name: Create base WordPress subdirectory Multisite config template: src: wordpress_multisite_subdirectories.conf.j2 @@ -85,5 +87,12 @@ mode: "u+rwx,g+rwxs,o-w" # http://brunogirin.blogspot.si/2010/03/shared-folders-in-ubuntu-with-setgid.html state: directory -- name: Set default ACL on web root to be group writeable - command: "setfacl -d -m u::rwx,g::rwx,o::r-x {{ www_root }}" +- 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 diff --git a/roles/nginx/templates/nginx.conf.j2 b/roles/nginx/templates/nginx.conf.j2 index da5dd573b0..6facf20419 100644 --- a/roles/nginx/templates/nginx.conf.j2 +++ b/roles/nginx/templates/nginx.conf.j2 @@ -1,106 +1,172 @@ # {{ ansible_managed }} -# nginx Configuration File -# http://wiki.nginx.org/Configuration +# Configuration File - Nginx Server Configs +# http://nginx.org/en/docs/dirindex.html -# Run as a less privileged user for security reasons. -user {{ nginx_user }}; +{% block modules_enabled -%} +include modules-enabled/*.conf; +{% endblock %} -# How many worker threads to run; -# "auto" sets it to the number of CPU cores available in the system, and -# offers the best performance. Don't set it higher than the number of CPU -# cores if changing this parameter. +{% block user %} +# Run as a unique, less privileged user for security reasons. +# Default: nobody nobody +user {{ nginx_user }}; +{% endblock %} -# The maximum number of connections for Nginx is calculated by: -# max_clients = worker_processes * worker_connections +{% block worker %} +# Sets the worker threads to the number of CPU cores available in the system for best performance. +# Should be > the number of CPU cores. +# Maximum number of connections = worker_processes * worker_connections +# Default: 1 worker_processes auto; -# Maximum open file descriptors per process; -# should be > worker_connections. +# Maximum number of open files per worker process. +# Should be > worker_connections. +# Default: no limit worker_rlimit_nofile 8192; +{% endblock %} +{% block events %} events { - # When you need > 8000 * cpu_cores connections, you start optimizing your OS, - # and this is probably the point at which you hire people who are smarter than - # you, as this is *a lot* of requests. + # If you need more connections than this, you start optimizing your OS. + # 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; } +{% endblock %} -# Default error log file -# (this is only used when you don't override error_log on a server{} level) +{% block error_log %} +# Log errors and warnings to this file +# This is only used when you don't override it on a server{} level +# Default: logs/error.log error error_log {{ nginx_logs_root }}/error.log warn; +{% endblock %} + +{% block pid %} +# The file storing the process ID of the main process +# Default: nginx.pid pid /run/nginx.pid; +{% endblock %} http { + {% block http_begin %}{% endblock %} + + {% block server_tokens -%} # Hide nginx version information. + # Default: on server_tokens off; + {% endblock %} + {% block cache -%} # Setup the fastcgi cache. fastcgi_buffers {{ nginx_fastcgi_buffers }}; fastcgi_buffer_size {{ nginx_fastcgi_buffer_size }}; + fastcgi_read_timeout {{ nginx_fastcgi_read_timeout }}; 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; + fastcgi_cache_key $realpath_root$scheme$host$request_uri$request_method$http_origin; fastcgi_ignore_headers Cache-Control Expires Set-Cookie; fastcgi_pass_header Set-Cookie; fastcgi_pass_header Cookie; + {% endblock %} - # Define the MIME types for files. + {% block mime_types -%} + # Specify MIME types for files. include h5bp-server-configs/mime.types; + + # Default: text/plain default_type application/octet-stream; + {% endblock %} - # Update charset_types due to updated mime.types - charset_types text/css text/plain text/vnd.wap.wml application/javascript application/json application/rss+xml application/xml; + {% block charset_types -%} + # Update charset_types to match updated mime.types. + # text/html is always included by charset module. + # Default: text/html text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml + charset_types + text/css + text/plain + text/vnd.wap.wml + application/javascript + application/json + application/rss+xml + application/xml; + {% endblock %} - # Format to use in log files + {% block log_format -%} + # Include $http_x_forwarded_for within default format used in log files log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; + {% endblock %} - # Default log file - # (this is only used when you don't override access_log on a server{} level) + {% block access_log -%} + # Log access to this file + # This is only used when you don't override it on a server{} level + # Default: logs/access.log combined access_log {{ nginx_logs_root }}/access.log main; + {% endblock %} - # How long to allow each connection to stay idle; longer values are better - # for each individual client, particularly for SSL, but means that worker - # connections are tied up longer. (Default: 65) - keepalive_timeout 20; + {% block keepalive -%} + # How long to allow each connection to stay idle. + # Longer values are better for each individual client, particularly for SSL, + # but means that worker connections are tied up longer. + # Default: 75s + keepalive_timeout 20s; + {% endblock %} + {% block sendfile -%} # Speed up file transfers by using sendfile() to copy directly # between descriptors rather than using read()/write(). + # For performance reasons, on FreeBSD systems w/ ZFS + # this option should be disabled as ZFS's ARC caches + # frequently used files in RAM by default. + # Default: off sendfile on; + {% endblock %} - # Tell Nginx not to send out partial frames; this increases throughput - # since TCP frames are filled up before being sent out. (adds TCP_CORK) + {% block tcp_nopush -%} + # Don't send out partial frames; this increases throughput + # since TCP frames are filled up before being sent out. + # Default: off tcp_nopush on; + {% endblock %} + {% block compression -%} # Compression - # Enable Gzip compressed. + # Enable gzip compression. + # Default: off gzip on; # Compression level (1-9). - # 5 is a perfect compromise between size and cpu usage, offering about - # 75% reduction for most ascii files (almost identical to level 9). + # 5 is a perfect compromise between size and CPU usage, offering about + # 75% reduction for most ASCII files (almost identical to level 9). + # Default: 1 gzip_comp_level 5; # Don't compress anything that's already small and unlikely to shrink much # if at all (the default is 20 bytes, which is bad as that usually leads to # larger files after gzipping). + # Default: 20 gzip_min_length 256; # Compress data even for clients that are connecting to us via proxies, # identified by the "Via" header (required for CloudFront). + # Default: off gzip_proxied any; # Tell proxies to cache both the gzipped and regular version of a resource # whenever the client's Accept-Encoding capabilities header varies; # Avoids the issue where a non-gzip capable client (which is extremely rare # today) would display gibberish if their proxy gave them the gzipped version. + # Default: off gzip_vary on; # Compress all output labeled with one of the following MIME-types. + # text/html is always compressed by gzip module. + # Default: text/html gzip_types application/atom+xml application/javascript @@ -126,15 +192,25 @@ http { text/vtt text/x-component text/x-cross-domain-policy; - # text/html is always compressed by HttpGzipModule # This should be turned on if you are going to have pre-compressed copies (.gz) of # static files available. If not it should be left off as it will cause extra I/O # for the check. It is best if you enable this in a location{} block for # a specific directory, or on an individual server{} level. # gzip_static on; - - include sites-enabled/*; + {% endblock %} + + {% block http_includes_d -%} + include includes.d/http/*.conf; + {% endblock -%} + + {% 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 + # by creating a symlink to it in the sites-enabled folder. + # See doc/sites-enabled.md for more info. + include sites-enabled/*.conf; + {% endblock %} {% if env != 'development' -%} # CloudFlare diff --git a/roles/nginx/templates/no-default.conf.j2 b/roles/nginx/templates/no-default.conf.j2 new file mode 100644 index 0000000000..3a9bff2df6 --- /dev/null +++ b/roles/nginx/templates/no-default.conf.j2 @@ -0,0 +1,14 @@ +# {{ ansible_managed }} + +# Drop requests for unknown hosts +# +# If no default server is defined, nginx will use the first found server. +# To prevent host header attacks, or other potential problems when an unknown +# servername is used in a request, it's recommended to drop the request +# returning 444 "no response". + +server { + listen [::]:80 default_server deferred; + listen 80 default_server deferred; + return 444; +} diff --git a/roles/php/defaults/main.yml b/roles/php/defaults/main.yml index 40531c89f9..066e952b49 100644 --- a/roles/php/defaults/main.yml +++ b/roles/php/defaults/main.yml @@ -1,6 +1,24 @@ disable_default_pool: true memcached_sessions: false +php_extensions_default: + php7.1-cli: "{{ apt_package_state }}" + php7.1-common: "{{ apt_package_state }}" + php7.1-curl: "{{ apt_package_state }}" + php7.1-dev: "{{ apt_package_state }}" + php7.1-fpm: "{{ apt_package_state }}" + php7.1-gd: "{{ apt_package_state }}" + php7.1-mbstring: "{{ apt_package_state }}" + php7.1-mcrypt: "{{ apt_package_state }}" + php7.1-mysql: "{{ apt_package_state }}" + php7.1-opcache: "{{ apt_package_state }}" + php7.1-xml: "{{ apt_package_state }}" + php7.1-xmlrpc: "{{ apt_package_state }}" + php7.1-zip: "{{ apt_package_state }}" + +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' php_display_startup_errors: 'Off' @@ -12,9 +30,11 @@ php_mysqlnd_collect_memory_statistics: 'Off' php_post_max_size: 50M php_sendmail_path: /usr/sbin/ssmtp -t 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_default_timezone: '{{ default_timezone }}' +php_timezone: '{{ ntp_timezone }}' php_opcache_enable: 1 php_opcache_enable_cli: 1 @@ -23,11 +43,3 @@ php_opcache_interned_strings_buffer: 8 php_opcache_max_accelerated_files: 4000 php_opcache_memory_consumption: 128 php_opcache_revalidate_freq: 60 - -php_xdebug_remote_enable: "false" -php_xdebug_remote_connect_back: "false" -php_xdebug_remote_host: localhost -php_xdebug_remote_port: "9000" -php_xdebug_remote_log: /tmp/xdebug.log -php_xdebug_idekey: XDEBUG -php_max_nesting_level: 200 diff --git a/roles/php/tasks/main.yml b/roles/php/tasks/main.yml index 30de686a8d..c0c14d9794 100644 --- a/roles/php/tasks/main.yml +++ b/roles/php/tasks/main.yml @@ -1,69 +1,24 @@ --- -- name: Add PHP 7.0 PPA +- name: Add PHP 7.1 PPA apt_repository: repo: "ppa:ondrej/php" update_cache: yes -- name: Install PHP 7.0 +- name: Install PHP 7.1 apt: - name: "{{ item }}" - state: present - force: yes - with_items: - - php7.0-cli - - php7.0-common - - php7.0-curl - - php7.0-dev - - php7.0-fpm - - php7.0-gd - - php7.0-mbstring - - php7.0-mcrypt - - php7.0-mysql - - php7.0-opcache - - php7.0-xml - - php7.0-xmlrpc - - php7.0-zip - - php7.0-soap - - php-imagick + name: "{{ item.key }}" + state: "{{ item.value }}" + cache_valid_time: "{{ apt_cache_valid_time }}" + with_dict: "{{ php_extensions }}" -- name: Install Xdebug - apt: - name: php-xdebug - state: latest - when: xdebug_install | default(false) - -- name: xdebug configuration file - template: - src: xdebug.ini.j2 - dest: /etc/php/7.0/mods-available/xdebug.ini - when: xdebug_install | default(false) - -- name: Start php7.0-fpm service +- name: Start php7.1-fpm service service: - name: php7.0-fpm + name: php7.1-fpm state: started enabled: true -- name: Create socket directory - file: - path: /var/run/php7.0-fpm/ - state: directory - -- name: Disable default pool - command: mv /etc/php/7.0/fpm/pool.d/www.conf /etc/php/7.0/fpm/pool.d/www.disabled - args: - creates: /etc/php/7.0/fpm/pool.d/www.disabled - when: disable_default_pool - notify: reload php-fpm - - name: PHP configuration file template: src: php.ini.j2 - dest: /etc/php/7.0/fpm/php.ini - notify: reload php-fpm - -- name: php-fpm configuration file - template: - src: php-fpm.conf.j2 - dest: /etc/php/7.0/fpm/pool.d/wordpress.conf + dest: /etc/php/7.1/fpm/php.ini notify: reload php-fpm diff --git a/roles/php/templates/php.ini.j2 b/roles/php/templates/php.ini.j2 index 0fcfe9da08..3b899e7c91 100644 --- a/roles/php/templates/php.ini.j2 +++ b/roles/php/templates/php.ini.j2 @@ -11,10 +11,12 @@ memory_limit = {{ php_memory_limit }} post_max_size = {{ php_post_max_size }} 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_default_timezone }} +date.timezone = {{ php_timezone }} [mysqlnd] mysqlnd.collect_memory_statistics = {{ php_mysqlnd_collect_memory_statistics }} diff --git a/roles/php/templates/xdebug.ini.j2 b/roles/php/templates/xdebug.ini.j2 deleted file mode 100644 index 8e8953bc7f..0000000000 --- a/roles/php/templates/xdebug.ini.j2 +++ /dev/null @@ -1,12 +0,0 @@ -; {{ ansible_managed }} - -[XDebug] -zend_extension="xdebug.so" -xdebug.remote_enable={{ php_xdebug_remote_enable }} -xdebug.remote_connect_back={{ php_xdebug_remote_connect_back }} -xdebug.remote_host={{ php_xdebug_remote_host }} -xdebug.remote_port={{ php_xdebug_remote_port }} -xdebug.remote_handler="dbgp" -xdebug.remote_log={{ php_xdebug_remote_log }} -xdebug.idekey="{{ php_xdebug_idekey }}" -xdebug.max_nesting_level = {{ php_max_nesting_level }} diff --git a/roles/remote-user/tasks/main.yml b/roles/remote-user/tasks/main.yml deleted file mode 100644 index 8ea687fe48..0000000000 --- a/roles/remote-user/tasks/main.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -- name: Require manual definition of remote-user - fail: - msg: | - When using `--ask-pass` option, use `-u` option to define remote-user: - ansible-playbook server.yml -e env={{ env }} -u root --ask-pass - when: ansible_user is not defined and cli_ask_pass | default(false) - -- name: Check whether Ansible can connect as root - local_action: command ansible {{ inventory_hostname }} -m raw -a whoami -u root {{ cli_options | default('') }} - failed_when: false - changed_when: false - register: root_status - tags: [connection-tests] - -- name: Set remote user for each host - set_fact: - ansible_user: "{{ ('root' in root_status.stdout_lines) | ternary('root', admin_user) }}" - when: ansible_user is not defined - -- name: Announce which user was selected - debug: - msg: "Note: Ansible will attempt connections as user = {{ ansible_user }}" diff --git a/roles/sshd/README.md b/roles/sshd/README.md index 97d3228920..f6d27bdcb6 100644 --- a/roles/sshd/README.md +++ b/roles/sshd/README.md @@ -1,56 +1,148 @@ -## What is ansible-sshd? - -It is an [ansible](http://www.ansible.com/home) role to install openssh-server and configure it. - -### What problem does it solve and why is it useful? - -Often times you want to disable root logins and password based logins. This role sets those options by default but it also exposes every config value found in the default ubuntu 14.04 `sshd_config` file. - -## Role variables - -Below is a list of default values along with a description of what they do. - -``` -# To view what these commands do, check out: -# http://www.openssh.com/cgi-bin/man.cgi?query=sshd_config - -sshd_port: 22 -sshd_listen_address: 0.0.0.0 -sshd_protocol: 2 -sshd_host_rsa_key: /etc/ssh/ssh_host_rsa_key -sshd_host_dsa_key: /etc/ssh/ssh_host_dsa_key -sshd_host_ecdsa_key: /etc/ssh/ssh_host_ecdsa_key -sshd_use_privilege_separation: true -sshd_key_regeneration_interval: 3600 -sshd_server_key_bits: 768 -sshd_syslog_facility: AUTH -sshd_log_level: INFO -sshd_login_grace_time: 120 -sshd_permit_root_login: true -sshd_strict_modes: true -sshd_rsa_authentication: true -sshd_pubkey_authentication: true -sshd_authorized_keys_file: "%h/.ssh/authorized_keys" -sshd_ignore_rhosts: true -sshd_rhosts_rsa_authentication: false -sshd_host_based_authentication: false -sshd_ignore_user_known_hosts: false -sshd_permit_empty_passwords: false -sshd_challenge_response_authentication: false -sshd_password_authentication: false -sshd_gss_api_authentication: false -sshd_gss_api_cleanup_credentials: true -sshd_x11_forwarding: true -sshd_x11_display_offset: 10 -sshd_print_motd: false -sshd_print_last_log: true -sshd_tcp_keep_alive: true -sshd_max_startups: 10:30:100 -sshd_banner: none -sshd_accept_env: LANG LC_* -sshd_subsystem: sftp /usr/lib/openssh/sftp-server -sshd_use_pam: true +The `sshd` role creates the following two configuration files on the server, setting secure defaults. + +* SSH server: `/etc/ssh/sshd_config` +* SSH client: `/etc/ssh/ssh_config` + +## Open a backup SSH connection + +When you modify SSH settings, you risk creating a configuration that blocks your future access. As a precaution, open a backup SSH connection in a second terminal before running the `sshd` role. Use this backup connection to resolve any configuration problems you encounter. Keep the connection active until you are confident your revised SSH configuration will allow you future access. + +``` +ssh -o ServerAliveInterval=60 root@12.34.56.78 +``` + +The `ServerAliveInterval` option causes your SSH client to periodically send the server messages that the connection is still alive. This helps prevent the server or your NAT router from pruning the connection as stale. If you are using PuTTY or WinSCP, change the "Seconds between keepalives" to 60. + +## Full configuration + +To keep the files as simple as possible, options are omitted if their system defaults are secure and broadly applicable. You may see the full and active configuration by running the following commands on your server. + +* SSH server (`sshd_config`): `sshd -T` +* SSH client (`ssh_config`): `ssh -G example.com` + +There are [resources](#resources) for understanding each option. + +## Customize via variables + +You may redefine any variable found in `templates/sshd_config.j2` or `templates/ssh_config.j2`. The default settings are viewable in `defaults/main.yml`. To override a setting, you could redefine your chosen variable in a file such as `group_vars/all/main.yml` or `group_vars/all/security.yml`. If you don't find a variable for the setting you need to change, you may need to [customize via child templates](#customize-via-child-templates). + +### Basic variable override + +Suppose you want your SSH server to `AcceptEnv`, whereas the Trellis default does not accept any env variables. You could find the relevant variable name in `templates/sshd_config.j2` or in `defaults/main.yml`, then redefine that variable in `group_vars/all/main.yml`. In this example, the relevant variable is `sshd_accept_env` and is formatted as a list. + +``` +# group_vars/all/main.yml + +sshd_accept_env: + - LANG + - LC_* +``` + +You may notice that `templates/ssh_config.j2` references some `ssh_` variables that are not included in `defaults/main.yml` and that default to a `sshd_` variable. Here is an example: +``` +AddressFamily {{ ssh_address_family | default(sshd_address_family) }} +``` +This pattern spares `defaults/main.yml` from having repetitious `ssh` and `sshd` definitions for all settings. You may still define custom values for any `ssh_` in your `group_vars` files. + +### `Ciphers`, `KexAlgorithms`, and `MACs` + +The variables for `Ciphers`, `KexAlgorithms`, and `MACs` are split into `_default` and `_extra` (e.g., `sshd_macs_default` and `sshd_macs_extra`). The `_default` contains a list you will probably not need to change. You may use `_extra` to supplement the default lists. SSH connections involving older systems may require some of the less secure options below. + +``` +# group_vars/all/security.yml + +# Allow CBC mode ciphers (less secure) +sshd_ciphers_extra: + - aes256-cbc + - aes192-cbc + - aes128-cbc + +# Accommodate older systems by allowing weaker kex algorithms (less secure) +sshd_kex_algorithms_extra: + - diffie-hellman-group14-sha1 + - diffie-hellman-group-exchange-sha1 + - diffie-hellman-group1-sha1 + +# Accommodate older systems by allowing weaker MACs (less secure) +sshd_macs_extra: + - umac-128@openssh.com + - hmac-sha1 +``` + +## Customize via child templates + +If you can't [customize via variables](#customize-via-variables) because the template doesn't include a variable for the setting you want to change, first check the [full configuration](#full-configuration) to verify that the default in effect is not what you want. If you need to make a change, you may create a child template to override the default template. + +Create your child templates following the [Jinja template inheritance](http://jinja.pocoo.org/docs/latest/templates/#template-inheritance) docs and the guidelines below. + + +### Designate a child template + +Use the `sshd_config` and `ssh_config` variables to inform Trellis of the child templates you have created. Below is an example of designating child templates in a new `templates` directory in your Trellis project root (e.g., next to the `server.yml` playbook). + +``` +# group_vars/all/main.yml + +sshd_config: "{{ playbook_dir }}/templates/sshd_config.j2" +ssh_config: "{{ playbook_dir }}/templates/ssh_config.j2" +``` + +### Create a child template + +Create your child templates at the paths you designated in the `sshd_config` and `ssh_config` variables described above. [Child templates](http://jinja.pocoo.org/docs/latest/templates/#child-template) must include two elements: + +* an `{% extends 'base_template' %}` statement +* one or more `{% block block_name %}` blocks + +The path for your base template – referenced in your `extends` statement – must be relative to the `server.yml` playbook (i.e., relative to the Trellis root directory). See the examples below. + +Here is an example child template that adds some sftp settings to the end of the `sshd_config`. + +``` +# templates/sshd_config.j2 + +{% extends 'roles/sshd/templates/sshd_config.j2' %} + +{% block main %} +{{ super() }} +Match Group sftponly +AllowAgentForwarding no +ChrootDirectory /home/%u +ForceCommand internal-sftp +PermitRootLogin no +{%- endblock %} ``` +The [`{{ super() }}`](http://jinja.pocoo.org/docs/latest/templates/#super-blocks) Jinja2 function returns the original block content from the base template, and can be omitted if you don't want to include the original content. + +Here is an example child template that adds host-specific SSH options at the beginning of `ssh_config`. + +``` +# templates/ssh_config.j2 + +{% extends 'roles/sshd/templates/ssh_config.j2' %} + +{% block main %} +# Host-specific configuration +Host example.com example2.com + Port 2222 + ForwardAgent yes + +# Global defaults for all Hosts +{{ super() }} +{%- endblock %} +``` + +## Troubleshooting + +See the Trellis docs for [troubleshooting SSH connections](https://roots.io/trellis/docs/troubleshooting/#ssh-connections). + +## Resources + +* Ubuntu manpage for [sshd_config](http://manpages.ubuntu.com/manpages/xenial/en/man5/sshd_config.5.html) +* Ubuntu manpage for [ssh_config](http://manpages.ubuntu.com/manpages/xenial/en/man5/ssh_config.5.html) +* stribika's [Secure Secure Shell](https://stribika.github.io/2015/01/04/secure-secure-shell.html) post +* MozillaWiki's [security guidelines for OpenSSH](https://wiki.mozilla.org/Security/Guidelines/OpenSSH) +* bettercrypto.org's [Applied Crypto Hardening](https://bettercrypto.org/static/applied-crypto-hardening.pdf) ## Attribution diff --git a/roles/sshd/defaults/main.yml b/roles/sshd/defaults/main.yml index 1c821320d6..deedd28e00 100644 --- a/roles/sshd/defaults/main.yml +++ b/roles/sshd/defaults/main.yml @@ -1,37 +1,99 @@ --- -sshd_port: 22 -sshd_listen_address: 0.0.0.0 +# sshd_config +# ---------------------------- +sshd_config: sshd_config.j2 + +sshd_ports: + - 22 + +sshd_listen_addresses: + - 0.0.0.0 + sshd_protocol: 2 -sshd_host_rsa_key: /etc/ssh/ssh_host_rsa_key -sshd_host_dsa_key: /etc/ssh/ssh_host_dsa_key -sshd_host_ecdsa_key: /etc/ssh/ssh_host_ecdsa_key -sshd_use_privilege_separation: true -sshd_key_regeneration_interval: 3600 -sshd_server_key_bits: 768 -sshd_syslog_facility: AUTH -sshd_log_level: INFO -sshd_login_grace_time: 120 -sshd_permit_root_login: true -sshd_strict_modes: true -sshd_rsa_authentication: true -sshd_pubkey_authentication: true -sshd_authorized_keys_file: "%h/.ssh/authorized_keys" -sshd_ignore_rhosts: true -sshd_rhosts_rsa_authentication: false -sshd_host_based_authentication: false -sshd_ignore_user_known_hosts: false -sshd_permit_empty_passwords: false + +sshd_accept_env: [] + +sshd_address_family: inet +sshd_allow_agent_forwarding: true +sshd_allow_tcp_forwarding: local sshd_challenge_response_authentication: false + +sshd_ciphers_default: + - chacha20-poly1305@openssh.com + - aes256-gcm@openssh.com + - aes128-gcm@openssh.com + - aes256-ctr + - aes192-ctr + - aes128-ctr + +sshd_ciphers_extra: [] + +sshd_client_alive_interval: 600 +sshd_debian_banner: false + +sshd_host_keys: + - /etc/ssh/ssh_host_ed25519_key + - /etc/ssh/ssh_host_rsa_key + +sshd_kex_algorithms_default: + - curve25519-sha256@libssh.org + - diffie-hellman-group-exchange-sha256 + +sshd_kex_algorithms_extra: [] + +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: [] + sshd_password_authentication: false -sshd_gss_api_authentication: false -sshd_gss_api_cleanup_credentials: true -sshd_x11_forwarding: true -sshd_x11_display_offset: 10 +sshd_permit_root_login: true +sshd_print_last_log: false sshd_print_motd: false -sshd_print_last_log: true -sshd_tcp_keep_alive: true -sshd_max_startups: 10:30:100 -sshd_banner: none -sshd_accept_env: LANG LC_* -sshd_subsystem: sftp /usr/lib/openssh/sftp-server +sshd_subsystem: sftp internal-sftp +sshd_tcp_keep_alive: false + +# PAM authentication enabled to avoid Debian bug with openssh-server. +# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=751636 +# can change to `false` once Canonical Main repository includes openssh 1:7.2p2-6 sshd_use_pam: true + +sshd_x11_forwarding: false + + +# ssh_config +# ---------------------------- +ssh_config: ssh_config.j2 +ssh_port: 22 +ssh_compression: true +ssh_gssapi_authentication: false + +ssh_host_key_algorithms: + - ssh-ed25519-cert-v01@openssh.com + - ssh-rsa-cert-v01@openssh.com + - ssh-ed25519 + - ssh-rsa + +ssh_identity_files: + - ~/.ssh/id_ed25519 + - ~/.ssh/id_rsa + +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/handlers/main.yml b/roles/sshd/handlers/main.yml index b04a143ca4..822887e31b 100644 --- a/roles/sshd/handlers/main.yml +++ b/roles/sshd/handlers/main.yml @@ -1,3 +1,5 @@ --- - name: restart ssh - service: name=ssh state=restarted \ No newline at end of file + service: + name: ssh + state: restarted diff --git a/roles/sshd/tasks/main.yml b/roles/sshd/tasks/main.yml index 8ddd1003d9..acabc23938 100644 --- a/roles/sshd/tasks/main.yml +++ b/roles/sshd/tasks/main.yml @@ -1,16 +1,29 @@ --- -- name: ensure ssh server is installed +- name: Ensure latest SSH server and client are installed apt: - pkg: openssh-server - state: latest - update_cache: true + name: "{{ item.key }}" + state: "{{ item.value }}" cache_valid_time: "{{ apt_cache_valid_time }}" - notify: - - restart ssh + with_dict: "{{ sshd_packages }}" + notify: restart ssh -- name: ensure sshd is configured +- name: Create a secure sshd_config template: - src: sshd_config.j2 + src: "{{ sshd_config }}" dest: /etc/ssh/sshd_config - notify: - - restart ssh + mode: 0600 + validate: '/usr/sbin/sshd -T -f %s' + notify: restart ssh + +- name: Create a secure ssh_config + template: + src: "{{ ssh_config }}" + dest: /etc/ssh/ssh_config + mode: 0644 + +- name: Remove Diffie-Hellman moduli of size < 2000 + lineinfile: + backup: yes + dest: /etc/ssh/moduli + regexp: ^(\d+\s){4}1 + state: absent diff --git a/roles/sshd/templates/ssh_config.j2 b/roles/sshd/templates/ssh_config.j2 new file mode 100644 index 0000000000..a21eee8c8f --- /dev/null +++ b/roles/sshd/templates/ssh_config.j2 @@ -0,0 +1,23 @@ +# {{ ansible_managed }} + +{% block main %} +Host * + Port {{ ssh_port }} + Protocol {{ ssh_protocol | default(sshd_protocol) }} + + AddressFamily {{ ssh_address_family | default(sshd_address_family) }} + ChallengeResponseAuthentication {{ ssh_challenge_response_authentication | default(sshd_challenge_response_authentication) | ternary('yes', 'no') }} + Ciphers {{ (ssh_ciphers_default | default(sshd_ciphers_default) + ssh_ciphers_extra | default(sshd_ciphers_extra)) | join(',') }} + Compression {{ ssh_compression | ternary('yes', 'no') }} + GSSAPIAuthentication {{ ssh_gssapi_authentication | ternary('yes', 'no') }} + HostKeyAlgorithms {{ ssh_host_key_algorithms | join(',') }} + {% for file in ssh_identity_files -%} + IdentityFile {{ file }} + {% endfor -%} + 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(' ') }} + 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 e0114d81c8..a1b961d7d4 100644 --- a/roles/sshd/templates/sshd_config.j2 +++ b/roles/sshd/templates/sshd_config.j2 @@ -1,38 +1,37 @@ # {{ ansible_managed }} -Port {{ sshd_port }} -ListenAddress {{ sshd_listen_address }} +{% block main %} +{% for port in sshd_ports %} +Port {{ port }} +{% endfor -%} + +AddressFamily {{ sshd_address_family }} + +{% for address in sshd_listen_addresses %} +ListenAddress {{ address }} +{% endfor -%} + Protocol {{ sshd_protocol }} -HostKey {{ sshd_host_rsa_key }} -HostKey {{ sshd_host_dsa_key }} -HostKey {{ sshd_host_ecdsa_key }} -UsePrivilegeSeparation {{ sshd_use_privilege_separation | ternary('yes', 'no') }} -KeyRegenerationInterval {{ sshd_key_regeneration_interval }} -ServerKeyBits {{ sshd_server_key_bits }} -SyslogFacility {{ sshd_syslog_facility }} -LogLevel {{ sshd_log_level }} -LoginGraceTime {{ sshd_login_grace_time }} -PermitRootLogin {{ sshd_permit_root_login | ternary('yes', 'no') }} -StrictModes {{ sshd_strict_modes | ternary('yes', 'no') }} -RSAAuthentication {{ sshd_rsa_authentication | ternary('yes', 'no') }} -PubkeyAuthentication {{ sshd_pubkey_authentication | ternary('yes', 'no') }} -AuthorizedKeysFile {{ sshd_authorized_keys_file }} -IgnoreRhosts {{ sshd_ignore_rhosts | ternary('yes', 'no') }} -RhostsRSAAuthentication {{ sshd_rhosts_rsa_authentication | ternary('yes', 'no') }} -HostbasedAuthentication {{ sshd_host_based_authentication | ternary('yes', 'no') }} -IgnoreUserKnownHosts {{ sshd_ignore_user_known_hosts | ternary('yes', 'no') }} -PermitEmptyPasswords {{ sshd_permit_empty_passwords | ternary('yes', 'no') }} + +AcceptEnv {{ sshd_accept_env | join(' ') }} +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') }} +Ciphers {{ (sshd_ciphers_default + sshd_ciphers_extra) | join(',') }} +ClientAliveInterval {{ sshd_client_alive_interval }} +DebianBanner {{ sshd_debian_banner | ternary('yes', 'no') }} +{% for key in sshd_host_keys %} +HostKey {{ key }} +{% endfor %} +KexAlgorithms {{ (sshd_kex_algorithms_default + sshd_kex_algorithms_extra) | join(',') }} +LoginGraceTime {{ sshd_login_grace_time }} +MACs {{ (sshd_macs_default + sshd_macs_extra) | join(',') }} PasswordAuthentication {{ sshd_password_authentication | ternary('yes', 'no') }} -GSSAPIAuthentication {{ sshd_gss_api_authentication | ternary('yes', 'no') }} -GSSAPICleanupCredentials {{ sshd_gss_api_cleanup_credentials | ternary('yes', 'no') }} -X11Forwarding {{ sshd_x11_forwarding | ternary('yes', 'no') }} -X11DisplayOffset {{ sshd_x11_display_offset }} -PrintMotd {{ sshd_print_motd | ternary('yes', 'no') }} +PermitRootLogin {{ sshd_permit_root_login | ternary('yes', 'no') }} PrintLastLog {{ sshd_print_last_log | ternary('yes', 'no') }} -TCPKeepAlive {{ sshd_tcp_keep_alive | ternary('yes', 'no') }} -MaxStartups {{ sshd_max_startups }} -Banner {{ sshd_banner }} -AcceptEnv {{ sshd_accept_env }} +PrintMotd {{ sshd_print_motd | ternary('yes', 'no') }} Subsystem {{ sshd_subsystem }} +TCPKeepAlive {{ sshd_tcp_keep_alive | ternary('yes', 'no') }} UsePAM {{ sshd_use_pam | ternary('yes', 'no') }} +X11Forwarding {{ sshd_x11_forwarding | ternary('yes', 'no') }} +{% endblock %} diff --git a/roles/ssmtp/defaults/main.yml b/roles/ssmtp/defaults/main.yml index d373337628..2c684dbacf 100644 --- a/roles/ssmtp/defaults/main.yml +++ b/roles/ssmtp/defaults/main.yml @@ -1,3 +1,4 @@ +ssmtp_package: ssmtp ssmtp_auth_method: LOGIN ssmtp_from_override: 'Yes' ssmtp_start_tls: 'Yes' diff --git a/roles/ssmtp/tasks/main.yml b/roles/ssmtp/tasks/main.yml index 036f98a30b..910451621a 100644 --- a/roles/ssmtp/tasks/main.yml +++ b/roles/ssmtp/tasks/main.yml @@ -1,8 +1,9 @@ --- - name: Install ssmtp apt: - name: ssmtp - state: present + name: "{{ ssmtp_package }}" + state: "{{ ssmtp_package_state | default(apt_package_state) }}" + cache_valid_time: "{{ apt_cache_valid_time }}" - name: ssmtp configuration template: diff --git a/roles/ssmtp/templates/ssmtp.conf.j2 b/roles/ssmtp/templates/ssmtp.conf.j2 index e7a5251aaa..14b5bd28cb 100644 --- a/roles/ssmtp/templates/ssmtp.conf.j2 +++ b/roles/ssmtp/templates/ssmtp.conf.j2 @@ -7,5 +7,9 @@ UseTLS={{ ssmtp_tls }} UseSTARTTLS={{ ssmtp_start_tls }} hostname={{ mail_hostname }} mailhub={{ mail_smtp_server }} +{% if mail_user is defined %} AuthUser={{ mail_user }} +{% endif %} +{% if mail_password is defined %} AuthPass={{ mail_password }} +{% endif %} diff --git a/roles/users/tasks/connection-warnings.yml b/roles/users/tasks/connection-warnings.yml new file mode 100644 index 0000000000..c8d505fc11 --- /dev/null +++ b/roles/users/tasks/connection-warnings.yml @@ -0,0 +1,40 @@ +--- +- name: Fail if root login will be disabled but admin_user cannot connect + fail: + msg: 'The admin_user `{{ admin_user }}` is unable to connect to the server. To prevent you from losing access to your server, the playbook has halted before disabling root login (`sshd_permit_root_login: false`). Ensure that the admin_user appears in your `users` hash with a valid entry for `keys`.' + when: not cli_ask_pass | default(false) and ansible_user == 'root' + +- block: + - name: Confirm that a non-root user can connect + pause: + prompt: | + + The play will disable SSH login for `root` (because `sshd_permit_root_login: false`) + but the admin_user named `{{ admin_user }}` appears unable to connect via SSH key. + + Be careful to avoid losing SSH access to your server. + Continue only if `{{ admin_user }}` will be able to connect via password or if + a different user will be able to connect and invoke sudo. + + (press RETURN to continue or CTRL+C to abort) + when: not sshd_permit_root_login and ansible_user == 'root' + + - name: Confirm disabling of SSH password authentication + pause: + prompt: | + + The play will disable password login (because `sshd_password_authentication: false`) + but the admin_user named `{{ admin_user }}` appears unable to connect via SSH key. + + Be careful to avoid losing SSH access to your server. + Continue only if you are certain you will have another means of connecting, + such as via SSH keys. + + If you prefer to continue to allow SSH password authentication (less secure), + abort now and make the following edit in `group_vars/all/security.yml`: + `sshd_password_authentication: true` + + (press RETURN to continue or CTRL+C to abort) + when: not sshd_password_authentication + + when: cli_ask_pass | default(false) diff --git a/roles/users/tasks/main.yml b/roles/users/tasks/main.yml index 8939abde5d..3c0435e40d 100644 --- a/roles/users/tasks/main.yml +++ b/roles/users/tasks/main.yml @@ -1,8 +1,9 @@ --- -- name: Ensure sudo group is present +- name: Ensure requested groups are present group: - name: sudo + name: "{{ item }}" state: present + with_items: "{{ users | sum(attribute='groups', start=[]) | list | unique }}" - name: Ensure sudo group has sudo privileges lineinfile: @@ -15,16 +16,24 @@ - name: Fail if root login will be disabled but admin_user will not be a sudoer assert: that: - - "{{ admin_user in (users | map(attribute='name') | list) }}" - - "{% for item in users if item.name == admin_user %}{{ 'sudo' in item.groups }}{% endfor %}" - msg: "When `sshd_permit_root_login: false`, you must add `sudo` to the `groups` for admin_user (in `users` hash), and set a password for admin_user in `sudoer_passwords`. Otherwise Ansible could lose the ability to run the necessary sudo commands." + - "{% for user in users if user.name == admin_user %}{{ 'sudo' in user.groups }}{% else %}{{ false }}{% endfor %}" + - "{% for user in vault_users | default([]) if user.name == admin_user %}{{ user.password is defined }}{% else %}{{ false }}{% endfor %}" + msg: | + When `sshd_permit_root_login: false`, you must add `sudo` to the `groups` for admin_user (in `users` hash), and set a password for admin_user in `vault_users` (in `group_vars/{{ env }}/vault.yml`). Otherwise Ansible could lose the ability to run the necessary sudo commands. {% if sudoer_passwords is defined or vault_sudoer_passwords is defined %} + + + Please note that `sudoer_passwords` and `vault_sudoer_passwords have been replaced with `vault_users`. {% endif %} + More info: + > https://roots.io/trellis/docs/security/#admin-user-sudoer-password when: not sshd_permit_root_login + tags: sshd - 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 %}' state: present shell: /bin/bash with_items: "{{ users }}" @@ -48,14 +57,14 @@ - keys - name: Check whether Ansible can connect as admin_user - local_action: command ansible {{ inventory_hostname }} -m ping -u {{ admin_user }} {{ cli_options_ping | default('') }} + local_action: command ansible {{ inventory_hostname }} -m ping -u {{ admin_user }} {{ cli_options | default('') }} failed_when: false changed_when: false become: no register: admin_user_status - when: not sshd_permit_root_login + when: (ansible_user != admin_user and not sshd_permit_root_login) or (cli_ask_pass and not sshd_password_authentication) + tags: [connection-tests, sshd] -- name: Fail if root login will be disabled but admin_user cannot connect - fail: - msg: 'The admin_user is unable to connect to the server. To prevent you from losing access to your server, the playbook has halted before disabling root login (`sshd_permit_root_login: false`). Ensure that the admin_user appears in your `users` hash with a valid entry for `keys`.' - when: not sshd_permit_root_login and admin_user_status | failed +- import_tasks: connection-warnings.yml + when: not admin_user_status | skipped and admin_user_status.rc != 0 + tags: [connection-tests, sshd] diff --git a/roles/wordpress-install/tasks/main.yml b/roles/wordpress-install/tasks/main.yml index 9cf0de7227..c79ca72b51 100644 --- a/roles/wordpress-install/tasks/main.yml +++ b/roles/wordpress-install/tasks/main.yml @@ -1,5 +1,5 @@ --- -- include: directories.yml +- import_tasks: directories.yml tags: wordpress-install-directories - name: Create .env file @@ -11,12 +11,28 @@ with_dict: "{{ wordpress_sites }}" - name: Copy .env file into web root - become: yes - become_user: "{{ web_user }}" command: rsync -ac --info=NAME /tmp/{{ item.key }}.env {{ www_root }}/{{ item.key }}/.env with_dict: "{{ wordpress_sites }}" register: env_file - changed_when: env_file.stdout == "{{ item.key }}.env" + changed_when: env_file.stdout == item.key + '.env' + +- name: Add known_hosts + known_hosts: + name: "{{ item.name }}" + key: "{{ item.key | default(omit) }}" + path: "{{ item.path | default(omit) }}" + state: "{{ item.state | default('present') }}" + become: no + with_items: "{{ known_hosts | default([]) }}" + +- 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 }}/" + no_log: true + when: item.value.packagist_token is defined + with_dict: "{{ wordpress_sites }}" - name: Download WP Core become: yes @@ -44,36 +60,30 @@ when: "{{ item.value.site_install | default(true) }} == True" - name: Install WP - command: wp core install - --allow-root - --url="{{ site_env.wp_home }}" - --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 }}" - args: - chdir: "{{ www_root }}/{{ item.key }}/" - register: wp_install_results - with_dict: "{{ wordpress_sites }}" - when: item.value.site_install | default(true) and not item.value.multisite.enabled | default(false) - changed_when: "'WordPress is already installed.' not in wp_install_results.stdout" - -- name: Install WP Multisite - command: wp core multisite-install + command: wp core {{ item.value.multisite.enabled | default(false) | ternary('multisite-install', 'install') }} --allow-root --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') }}" + {% 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 }}" args: chdir: "{{ www_root }}/{{ item.key }}/" - register: wp_install_results + register: wp_install with_dict: "{{ wordpress_sites }}" - when: item.value.site_install | default(true) and item.value.multisite.enabled | default(false) - changed_when: "'The network already exists.' not in wp_install_results.stdout" + 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" + +- name: Setup Permalink Structure + command: wp rewrite structure {{ item.item.value.initial_permalink_structure | default("/%postname%/") }} --allow-root + args: + chdir: "{{ www_root }}/{{ item.item.key }}/" + with_items: "{{ wp_install.results }}" + when: item | changed - name: Update WP Multisite Home URL command: wp option update home {{ site_env.wp_home }} --allow-root diff --git a/roles/wordpress-setup/defaults/main.yml b/roles/wordpress-setup/defaults/main.yml index dca1ba5052..b34865ca89 100644 --- a/roles/wordpress-setup/defaults/main.yml +++ b/roles/wordpress-setup/defaults/main.yml @@ -1 +1,43 @@ site_uses_local_db: "{{ site_env.db_host == 'localhost' }}" +nginx_wordpress_site_conf: wordpress-site.conf.j2 +nginx_ssl_path: "{{ nginx_path }}/ssl" + +# HSTS defaults +nginx_hsts_max_age: 31536000 +nginx_hsts_include_subdomains: true +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) }}" + +# 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 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_d_cleanup: true + +# h5bp helpers +not_dev: "{{ env != 'development' }}" +h5bp: "{{ item.value.h5bp | default({}) }}" +h5bp_cache_file_descriptors_enabled: "{{ h5bp.cache_file_descriptors | default(not_dev) }}" +h5bp_extra_security_enabled: "{{ h5bp.extra_security | default(true) }}" +h5bp_no_transform_enabled: "{{ h5bp.no_transform | default(false) }}" +h5bp_x_ua_compatible_enabled: "{{ h5bp.x_ua_compatible | default(true) }}" +h5bp_cache_busting_enabled: "{{ h5bp.cache_busting | default(false) }}" +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) }}" + +# PHP FPM +php_fpm_pm_max_children: 10 +php_fpm_pm_start_servers: 1 +php_fpm_pm_min_spare_servers: 1 +php_fpm_pm_max_spare_servers: 3 +php_fpm_pm_max_requests: 500 diff --git a/roles/wordpress-setup/tasks/database.yml b/roles/wordpress-setup/tasks/database.yml index 63db5b5cbc..61c74a5475 100644 --- a/roles/wordpress-setup/tasks/database.yml +++ b/roles/wordpress-setup/tasks/database.yml @@ -1,42 +1,25 @@ --- -- name: Create database of 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 }}" - when: site_uses_local_db and item.value.db_create | default(True) +- block: + - 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 }}" -- name: Create/assign database user to db and grant permissions - mysql_user: - name: "{{ site_env.db_user }}" - password: "{{ site_env.db_password }}" - append_privs: yes - priv: "{{ site_env.db_name }}.*:ALL" - state: present - login_host: "{{ site_env.db_host }}" - login_user: "{{ mysql_root_user }}" - login_password: "{{ mysql_root_password }}" - with_dict: "{{ wordpress_sites }}" - when: site_uses_local_db and item.value.db_create | default(True) + - name: Create/assign database user to db and grant permissions + mysql_user: + name: "{{ site_env.db_user }}" + password: "{{ site_env.db_password }}" + host: "{{ site_env.db_user_host }}" + append_privs: yes + priv: "{{ site_env.db_name }}.*:ALL" + state: present + login_host: "{{ site_env.db_host }}" + login_user: "{{ mysql_root_user }}" + login_password: "{{ mysql_root_password }}" + with_dict: "{{ wordpress_sites }}" -- name: Copy database dump - copy: - src: "{{ item.value.db_import }}" - dest: /tmp - with_dict: "{{ wordpress_sites }}" - when: item.value.db_import | default(False) - -- name: Import database - mysql_db: - name: "{{ site_env.db_name }}" - state: import - target: "/tmp/{{ item.value.db_import | basename }}" - login_host: "{{ site_env.db_host }}" - login_user: "{{ site_env.db_user }}" - login_password: "{{ site_env.db_password }}" - with_dict: "{{ wordpress_sites }}" - when: item.value.db_import | default(False) - notify: reload nginx + 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 2c79028794..3f2b8524c3 100644 --- a/roles/wordpress-setup/tasks/main.yml +++ b/roles/wordpress-setup/tasks/main.yml @@ -1,8 +1,10 @@ --- -- include: database.yml +- import_tasks: database.yml tags: wordpress-setup-database -- include: self-signed-certificate.yml +- import_tasks: self-signed-certificate.yml tags: wordpress-setup-self-signed-certificate +- import_tasks: nginx-client-cert.yml + tags: wordpress-setup-nginx-client-cert - name: Create logs folder of sites file: @@ -13,7 +15,23 @@ state: directory with_dict: "{{ wordpress_sites }}" -- include: nginx.yml +- name: Create WordPress php-fpm configuration file + template: + src: php-fpm.conf.j2 + dest: /etc/php/7.1/fpm/pool.d/wordpress.conf + 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 + args: + creates: /etc/php/7.1/fpm/pool.d/www.disabled + when: disable_default_pool | default(true) + notify: reload php-fpm + +- import_tasks: nginx-includes.yml + tags: [nginx-includes, wordpress-setup-nginx] + +- import_tasks: nginx.yml tags: wordpress-setup-nginx - name: Setup WP system cron @@ -21,7 +39,17 @@ name: "{{ item.key }} WordPress cron" minute: "*/15" user: "{{ web_user }}" - job: "curl -k -s {{ site_env.wp_siteurl }}/wp-cron.php > /dev/null 2>&1" + 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('.', '_') }}" + state: "{{ (cron_enabled and not item.value.multisite.enabled) | ternary('present', 'absent') }}" + with_dict: "{{ wordpress_sites }}" + +- name: Setup WP Multisite system cron + cron: + name: "{{ item.key }} WordPress network cron" + minute: "*/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" + cron_file: "wordpress-multisite-{{ item.key | replace('.', '_') }}" + state: "{{ (cron_enabled and item.value.multisite.enabled) | ternary('present', 'absent') }}" with_dict: "{{ wordpress_sites }}" - when: site_env.disable_wp_cron and not item.value.multisite.enabled | default(false) diff --git a/roles/wordpress-setup/tasks/nginx-client-cert.yml b/roles/wordpress-setup/tasks/nginx-client-cert.yml new file mode 100644 index 0000000000..49d810eee5 --- /dev/null +++ b/roles/wordpress-setup/tasks/nginx-client-cert.yml @@ -0,0 +1,8 @@ +--- +- name: Download client cert + 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 diff --git a/roles/wordpress-setup/tasks/nginx-includes.yml b/roles/wordpress-setup/tasks/nginx-includes.yml new file mode 100644 index 0000000000..a9859b0afb --- /dev/null +++ b/roles/wordpress-setup/tasks/nginx-includes.yml @@ -0,0 +1,54 @@ +--- +- name: Build list of Nginx includes templates + find: + paths: + - "{{ nginx_includes_templates_path }}" + - "{{ nginx_includes_deprecated }}" + pattern: "*.conf.j2" + recurse: yes + become: no + connection: local + register: nginx_includes_templates + +- 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 + +- 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: 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: 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: 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 diff --git a/roles/wordpress-setup/tasks/nginx.yml b/roles/wordpress-setup/tasks/nginx.yml index 1ef7378b56..e2d9b58cc8 100644 --- a/roles/wordpress-setup/tasks/nginx.yml +++ b/roles/wordpress-setup/tasks/nginx.yml @@ -5,7 +5,8 @@ dest: "{{ nginx_ssl_path }}/{{ item.value.ssl.cert | basename }}" mode: 0640 with_dict: "{{ wordpress_sites }}" - when: item.value.ssl.enabled and item.value.ssl.cert is defined + when: ssl_enabled and item.value.ssl.cert is defined + notify: reload nginx - name: Copy SSL key copy: @@ -13,39 +14,10 @@ dest: "{{ nginx_ssl_path }}/{{ item.value.ssl.key | basename }}" mode: 0600 with_dict: "{{ wordpress_sites }}" - when: item.value.ssl.enabled and item.value.ssl.key is defined - -- name: Create includes.d directories - file: - path: "{{ nginx_path }}/includes.d/{{ item }}" - state: directory - mode: 0755 - with_items: "{{ wordpress_sites.keys() }}" - register: nginx_includes_paths - -- name: Template files out to includes.d - template: - src: "includes.d/{{ item }}" - dest: "{{ nginx_path }}/includes.d/{{ item[:-3] }}" - with_lines: "cd {{ role_path }}/templates/includes.d && find {{ wordpress_sites.keys() | join(' ') }} -type f -name \\*.conf.j2 2>/dev/null || :" - register: nginx_includes_managed + when: ssl_enabled and item.value.ssl.key is defined notify: reload nginx -- name: Retrieve list of existing files in includes.d - shell: "find {{ nginx_includes_paths.results | map(attribute='path') | join(' ') }} -type f -name \\*.conf 2>/dev/null || :" - register: nginx_includes_existing - changed_when: false - -- name: Remove unmanaged files from includes.d - file: - path: "{{ item }}" - state: absent - with_items: "{{ nginx_includes_existing.stdout_lines | - difference(nginx_includes_managed.results | default([]) | map(attribute='item') | - map('regex_replace', '(.*)\\.j2', '/etc/nginx/includes.d/\\1') | list - ) - }}" - notify: reload nginx +- import_tasks: "{{ playbook_dir }}/roles/common/tasks/disable_challenge_sites.yml" - name: Create Nginx conf for challenges location template: @@ -55,10 +27,11 @@ - name: Create WordPress configuration for Nginx template: - src: "wordpress-site.conf.j2" + src: "{{ item.value.nginx_wordpress_site_conf | default(nginx_wordpress_site_conf) }}" dest: "{{ nginx_path }}/sites-available/{{ item.key }}.conf" with_dict: "{{ wordpress_sites }}" notify: reload nginx + tags: nginx-includes - name: Enable WordPress site file: diff --git a/roles/wordpress-setup/tasks/self-signed-certificate.yml b/roles/wordpress-setup/tasks/self-signed-certificate.yml index 9dda3e33d5..80c6600cb1 100644 --- a/roles/wordpress-setup/tasks/self-signed-certificate.yml +++ b/roles/wordpress-setup/tasks/self-signed-certificate.yml @@ -1,13 +1,23 @@ --- - name: Generate self-signed certificates - shell: > - openssl req -subj "/CN={{ item.value.site_hosts | first }}" -new - -newkey rsa:2048 -days 3650 -nodes -x509 -sha256 - -keyout {{ item.key }}.key -out {{ item.key }}.cert + 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 + ) \ + -keyout {{ item.key | quote }}.key -out {{ item.key | quote }}.cert" args: - chdir: "{{ nginx_path }}/ssl" + executable: "/bin/bash" + chdir: "{{ nginx_ssl_path }}" creates: "{{ item.key }}.*" with_dict: "{{ wordpress_sites }}" - when: item.value.ssl.enabled and item.value.ssl.provider | default('manual') == 'self-signed' - notify: - - reload nginx + when: ssl_enabled and item.value.ssl.provider | default('manual') == 'self-signed' + notify: reload nginx diff --git a/roles/wordpress-setup/templates/https.conf.j2 b/roles/wordpress-setup/templates/https.conf.j2 deleted file mode 100644 index edc908c7a8..0000000000 --- a/roles/wordpress-setup/templates/https.conf.j2 +++ /dev/null @@ -1,22 +0,0 @@ -include h5bp/directive-only/ssl.conf; -include h5bp/directive-only/ssl-stapling.conf; - -ssl_dhparam /etc/nginx/ssl/dhparams.pem; -ssl_buffer_size 1400; # 1400 bytes to fit in one MTU - -{% set hsts_max_age = item.value.ssl.hsts_max_age | default(nginx_hsts_max_age) %} -{% set hsts_include_subdomains = item.value.ssl.hsts_include_subdomains | default(nginx_hsts_include_subdomains) | ternary('includeSubdomains', None) %} -{% set hsts_preload = item.value.ssl.hsts_preload | default(nginx_hsts_preload) | ternary('preload', None) %} -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') == 'manual' and item.value.ssl.cert is defined and item.value.ssl.key is defined -%} - 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 }}-bundled.cert; - ssl_certificate_key {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}.key; -{%- elif item.value.ssl.provider | default('manual') == 'self-signed' -%} - ssl_certificate {{ nginx_path }}/ssl/{{ item.key }}.cert; - ssl_trusted_certificate {{ nginx_path }}/ssl/{{ item.key }}.cert; - ssl_certificate_key {{ nginx_path }}/ssl/{{ item.key }}.key; -{%- endif -%} \ No newline at end of file diff --git a/roles/php/templates/php-fpm.conf.j2 b/roles/wordpress-setup/templates/php-fpm.conf.j2 similarity index 65% rename from roles/php/templates/php-fpm.conf.j2 rename to roles/wordpress-setup/templates/php-fpm.conf.j2 index b1ea80ec40..1464745633 100644 --- a/roles/php/templates/php-fpm.conf.j2 +++ b/roles/wordpress-setup/templates/php-fpm.conf.j2 @@ -7,11 +7,11 @@ listen.group = www-data user = {{ web_user }} group = {{ web_group }} pm = dynamic -pm.max_children = 10 -pm.start_servers = 1 -pm.min_spare_servers = 1 -pm.max_spare_servers = 3 -pm.max_requests = 500 +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 }} +pm.max_spare_servers = {{ php_fpm_pm_max_spare_servers }} +pm.max_requests = {{ php_fpm_pm_max_requests }} chdir = {{ www_root }}/ php_flag[log_errors] = on php_flag[display_errors] = {{ php_display_errors }} diff --git a/roles/wordpress-setup/templates/wordpress-site.conf.j2 b/roles/wordpress-setup/templates/wordpress-site.conf.j2 index cf06de6505..882e1516fb 100644 --- a/roles/wordpress-setup/templates/wordpress-site.conf.j2 +++ b/roles/wordpress-setup/templates/wordpress-site.conf.j2 @@ -1,77 +1,238 @@ # {{ ansible_managed }} -server { - {% if item.value.ssl is defined and item.value.ssl.enabled | default(false) -%} - listen 443 ssl http2; - {% else -%} - listen 80; - {% endif %} +{% block server_before %}{% endblock %} - server_name {% for host in item.value.site_hosts %} {{ host }} {% if item.value.multisite.subdomains | default(false) %} *.{{ host }} {% endif %} {% endfor %}; - access_log {{ www_root }}/logs/{{ item.key }}/access.log; +server { + {% block server_id -%} + listen {{ ssl_enabled | ternary('[::]:443 ssl http2', '[::]:80') }}; + listen {{ ssl_enabled | ternary('443 ssl http2', '80') }}; + server_name {{ site_hosts_canonical | union(multisite_subdomains_wildcards) | join(' ') }}; + {% endblock %} + + {% block logs -%} + access_log {{ www_root }}/logs/{{ item.key }}/access.log main; error_log {{ www_root }}/logs/{{ item.key }}/error.log; + {% endblock %} + {% block server_basic -%} root {{ www_root }}/{{ item.key }}; index index.php index.htm index.html; + add_header Fastcgi-Cache $upstream_cache_status; + # Specify a charset charset utf-8; + # Set the max body size equal to PHP's max POST size. + client_max_body_size {{ php_post_max_size | default('25m') | lower }}; + {% if env == 'development' -%} - # See Virtualbox section at http://wiki.nginx.org/Pitfalls + # https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#virtualbox sendfile off; - {%- else -%} - include set-cache-uri.conf; - {%- endif %} + {% endif -%} + {% endblock -%} + + {% block cache_conditions -%} + {% if item.value.cache is defined and item.value.cache.enabled | default(false) -%} + # Fastcgi cache conditions + set $skip_cache 0; + if ($query_string != "") { + set $skip_cache 1; + } + if ($request_uri ~* "{{ item.value.cache.skip_cache_uri | default(nginx_skip_cache_uri) }}") { + set $skip_cache 1; + } + if ($http_cookie ~* "{{ item.value.cache.skip_cache_cookie | default(nginx_skip_cache_cookie) }}") { + set $skip_cache 1; + } + + {% endif -%} + {% endblock -%} + + {% block multisite_rewrites -%} {% if item.value.multisite.enabled | default(false) -%} - {% if not item.value.multisite.subdomains | default(false) -%} - include wordpress_multisite_subdirectories.conf; - {%- endif %} - {%- endif %} + # Multisite rewrites + {% if item.value.multisite.subdomains | default(false) -%} + rewrite ^/(wp-.*.php)$ /$1 last; + rewrite ^/(wp-(content|admin|includes).*) /$1 last; - add_header Fastcgi-Cache $upstream_cache_status; + {% else -%} + if (!-e $request_filename) { + rewrite /wp-admin$ $scheme://$host$uri/ permanent; + rewrite ^(/[^/]+)?(/wp-.*) $2 last; + rewrite ^(/[^/]+)?(/.*\.php) $2 last; + } - {% if item.value.ssl is defined and item.value.ssl.enabled | default(false) -%} - {{ lookup('template', 'https.conf.j2') }} - {% endif %} + {% endif -%} + {% endif -%} + {% endblock -%} - {% if item.value.ssl is not defined or not item.value.ssl.enabled | default(false) -%} - include acme-challenge-location.conf; - {% endif %} + {% block https -%} + {% 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.client_cert_url is defined -%} + 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 -%} + 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_key {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}.key; + + {% elif item.value.ssl.provider | default('manual') == 'self-signed' -%} + ssl_certificate {{ nginx_path }}/ssl/{{ item.key }}.cert; + ssl_trusted_certificate {{ nginx_path }}/ssl/{{ item.key }}.cert; + ssl_certificate_key {{ nginx_path }}/ssl/{{ item.key }}.key; + + {% endif -%} + {% endif -%} + {% endblock -%} + + {% block acme_challenge -%} + include acme-challenge-location.conf; + + {% endblock -%} + + {% block includes_d -%} include includes.d/{{ item.key }}/*.conf; - include wordpress.conf; + {% endblock -%} + + {% block location_uploads_php -%} + # Prevent PHP scripts from being executed inside the uploads folder. + location ~* /app/uploads/.*\.php$ { + deny all; + } + {% endblock %} + + {% block location_primary -%} + location / { + try_files $uri $uri/ /index.php?$args; + } + {% endblock %} + + {% block h5bp -%} + {% if h5bp_cache_file_descriptors_enabled -%} + include h5bp/directive-only/cache-file-descriptors.conf; + {% endif -%} + + {% if h5bp_extra_security_enabled -%} + include h5bp/directive-only/extra-security.conf; + {% endif -%} + + {% if h5bp_no_transform_enabled -%} + include h5bp/directive-only/no-transform.conf; + {% endif -%} + + {% if h5bp_x_ua_compatible_enabled -%} + include h5bp/directive-only/x-ua-compatible.conf; + {% endif -%} + + {% if h5bp_cache_busting_enabled -%} + include h5bp/location/cache-busting.conf; + {% endif -%} + + {% if h5bp_cross_domain_fonts_enabled -%} + include h5bp/location/cross-domain-fonts.conf; + {% endif -%} + + {% if h5bp_expires_enabled -%} + include h5bp/location/expires.conf; + {% endif -%} + + {% if h5bp_protect_system_files_enabled -%} + include h5bp/location/protect-system-files.conf; + {% endif -%} + + {% endblock %} + + {% block location_php -%} location ~ \.php$ { - try_files $uri =404; - error_page 404 /index.php; + {% block location_php_basic -%} + try_files $uri /index.php; + {% endblock -%} + + {% block cache_config -%} {% if item.value.cache is defined and item.value.cache.enabled | default(false) -%} - set $skip_cache 0; - - if ($query_string != "") { - set $skip_cache 1; - } - - # Don't cache uris containing the following segments - if ($request_uri ~* "{{ item.value.cache.skip_cache_uri | default(nginx_skip_cache_uri) }}") { - set $skip_cache 1; - } - - # Don't use the cache if cookies includes the following - if ($http_cookie ~* "{{ item.value.cache.skip_cache_cookie | default(nginx_skip_cache_cookie) }}") { - set $skip_cache 1; - } - - 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 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; + {% endif -%} + {% endblock -%} + {% block fastcgi_basic -%} include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; fastcgi_pass unix:/var/run/php-fpm-wordpress.sock; + {%- endblock %} + + } + {%- endblock %} + +} + +{% block redirects_https %} +{% if ssl_enabled %} +# Redirect to https +server { + listen [::]:80; + listen 80; + server_name {{ site_hosts_canonical | union(multisite_subdomains_wildcards) | join(' ') }}; + + {{ self.acme_challenge() -}} + + {{ self.includes_d() -}} + + location / { + return 301 https://$host$request_uri; + } +} + +{% endif %} +{% endblock -%} + +{%- block redirects_domains %} +{% if site_hosts_redirects | default([]) | count %} +# 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; + {% endif -%} + listen [::]:80; + listen 80; + server_name {{ host.redirects | join(' ') }}; + + {{ self.https() -}} + + {{ self.acme_challenge() -}} + + {{ self.includes_d() -}} + + location / { + return 301 {{ ssl_enabled | ternary('https', 'http') }}://{{ host.canonical }}$request_uri; } } +{% endfor %} +{% endblock %} diff --git a/roles/wp-cli/defaults/main.yml b/roles/wp-cli/defaults/main.yml index 3701261cc6..fcc8ab74d9 100644 --- a/roles/wp-cli/defaults/main.yml +++ b/roles/wp-cli/defaults/main.yml @@ -1,4 +1,6 @@ +wp_cli_version: 1.4.1 wp_cli_bin_path: /usr/bin/wp -wp_cli_phar_url: "https://github.com/wp-cli/wp-cli/releases/download/v1.1.0/wp-cli-1.1.0.phar" -wp_cli_completion_url: "https://raw.githubusercontent.com/wp-cli/wp-cli/master/utils/wp-completion.bash" -wp_cli_completion_path: /etc/bash_completion.d +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_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 4021a1e3bc..c3f6770953 100644 --- a/roles/wp-cli/tasks/main.yml +++ b/roles/wp-cli/tasks/main.yml @@ -1,12 +1,34 @@ --- -- name: Install WP-CLI +- name: Download WP-CLI get_url: url: "{{ wp_cli_phar_url }}" - dest: "{{ wp_cli_bin_path }}" - mode: 0755 + dest: /tmp/wp-cli-{{ wp_cli_version }}.phar + +- 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' + +- name: Retrieve WP-CLI tab completions + 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 - get_url: - url: "{{ wp_cli_completion_url }}" - dest: "{{ wp_cli_completion_path }}" - mode: 0644 + 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_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 }}" diff --git a/roles/xdebug-tunnel/defaults/main.yml b/roles/xdebug-tunnel/defaults/main.yml new file mode 100644 index 0000000000..c9a52a144a --- /dev/null +++ b/roles/xdebug-tunnel/defaults/main.yml @@ -0,0 +1,10 @@ +xdebug_tunnel_remote_port: 9000 +xdebug_tunnel_host: localhost +xdebug_tunnel_local_port: 9000 +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_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 new file mode 100644 index 0000000000..b015c11099 --- /dev/null +++ b/roles/xdebug-tunnel/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- 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 }}' + {% else %} + ssh -S '{{ xdebug_tunnel_control_socket }}' -O exit '{{ xdebug_tunnel_control_identity }}' + {% endif %} + connection: local + become: no + register: xdebug_tunnel + ignore_errors: true + +- name: Interpret and present Xdebug SSH tunnel errors + fail: + msg: | + {% if 'already' in xdebug_tunnel.stderr | default('') %} + SSH tunnel already exists! Refer to TODO for help. + {% elif 'No such file or directory' in xdebug_tunnel.stderr | default('') %} + 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('') + +- name: Announce Xdebug SSH tunnel status + debug: + msg: SSH Tunnel was {{ xdebug_remote_enable | bool | ternary('created', 'closed') }}! + when: xdebug_tunnel | changed diff --git a/roles/xdebug/defaults/main.yml b/roles/xdebug/defaults/main.yml new file mode 100644 index 0000000000..b210759a30 --- /dev/null +++ b/roles/xdebug/defaults/main.yml @@ -0,0 +1,43 @@ +php_xdebug_package: php-xdebug + +# 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_idekey: XDEBUG +xdebug_extended_info: 1 +xdebug_max_nesting_level: 200 + +# XDebug Display Settings +xdebug_force_display_errors: 0 +xdebug_force_error_reporting: 0 +xdebug_scream: 0 +xdebug_var_display_max_children: 128 +xdebug_var_display_max_data: 512 +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 new file mode 100644 index 0000000000..1a3061426c --- /dev/null +++ b/roles/xdebug/tasks/main.yml @@ -0,0 +1,34 @@ +--- +- block: + - 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: Template the Xdebug configuration file + template: + src: xdebug.ini.j2 + dest: /etc/php/7.1/mods-available/xdebug.ini + notify: reload php-fpm + + - 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: Disable Xdebug + file: + path: /etc/php/7.1/fpm/conf.d/20-xdebug.ini + state: absent + when: not xdebug_remote_enable | bool + notify: reload php-fpm + +- name: Disable Xdebug CLI + file: + path: /etc/php/7.1/cli/conf.d/20-xdebug.ini + state: absent diff --git a/roles/xdebug/templates/xdebug.ini.j2 b/roles/xdebug/templates/xdebug.ini.j2 new file mode 100644 index 0000000000..72435a2bf8 --- /dev/null +++ b/roles/xdebug/templates/xdebug.ini.j2 @@ -0,0 +1,47 @@ +; {{ ansible_managed }} + +[XDebug] +zend_extension=xdebug.so + +; 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.idekey={{ xdebug_idekey }} +xdebug.extended_info={{ xdebug_extended_info }} +xdebug.max_nesting_level={{ xdebug_max_nesting_level }} + +; Display Settings +xdebug.force_display_errors={{ xdebug_force_display_errors }} +xdebug.force_error_reporting={{ xdebug_force_error_reporting }} +xdebug.scream={{ xdebug_scream }} +xdebug.var_display_max_children={{ xdebug_var_display_max_children }} +xdebug.var_display_max_data={{ xdebug_var_display_max_data }} +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/server.yml b/server.yml index c6368cea1e..c88a85092b 100644 --- a/server.yml +++ b/server.yml @@ -1,15 +1,25 @@ --- -- include: variable-check.yml +- import_playbook: variable-check.yml vars: playbook: server.yml -- name: Determine Remote User +- name: Test Connection and Determine Remote User hosts: web:&{{ env }} gather_facts: false roles: - - { role: remote-user, tags: [remote-user, always] } + - { role: connection, tags: [connection, always] } -- name: WordPress Server - Install LEMP Stack with PHP 7.0 and MariaDB MySQL +- 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 hosts: web:&{{ env }} become: yes roles: @@ -29,7 +39,7 @@ - { role: composer, tags: [composer] } - { role: ansible, tags: [ansible], when: env == "sandbox" } - { role: wp-cli, tags: [wp-cli] } - - { role: letsencrypt, tags: [letsencrypt], when: letsencrypt_enabled } + - { role: letsencrypt, tags: [letsencrypt], when: sites_using_letsencrypt | count } - { role: wordpress-setup, tags: [wordpress, wordpress-setup, letsencrypt] } - { role: wordpress-install, tags: [wordpress, wordpress-install] } - { role: static-sites-setup, tags: [static-sites-setup], when: env != "sandbox" } diff --git a/vagrant.default.yml b/vagrant.default.yml new file mode 100644 index 0000000000..bd79ec9bab --- /dev/null +++ b/vagrant.default.yml @@ -0,0 +1,24 @@ +--- +vagrant_ip: '192.168.50.5' +vagrant_cpus: 1 +vagrant_memory: 1024 # in MB +vagrant_box: 'bento/ubuntu-16.04' +vagrant_box_version: '<= 201710.25.0' +vagrant_ansible_version: '2.4.2.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/vagrant.local.yml b/vagrant.local.yml new file mode 100644 index 0000000000..59c2361ba0 --- /dev/null +++ b/vagrant.local.yml @@ -0,0 +1,2 @@ +--- +vagrant_ip: '192.168.51.62' diff --git a/windows.sh b/windows.sh deleted file mode 100644 index b74c1f764d..0000000000 --- a/windows.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# -# Windows provisioner for Trellis -# heavily modified and based on KSid/windows-vagrant-ansible -# @author Andrea Brandi -# @version 1.0 - -ANSIBLE_PATH="$(find /vagrant -name 'windows.sh' -printf '%h' -quit)" -export PYTHONUNBUFFERED=1 - -# Create an ssh key if not already created. -if [ ! -f ~/.ssh/id_rsa ]; then - echo -e "\n\n\n" | ssh-keygen -t rsa -fi - -# Check SSH forwarding agent -echo ' -printf "\033[1;33m" -if ! ssh-add -l >/dev/null; then - printf "See: https://roots.io/trellis/docs/windows/#ssh-forwarding" -fi -printf "\033[0m\n\n" -' >> /home/vagrant/.profile - -# Check that add-apt-repository is installed for non-standard Vagrant boxes -if [ ! -f /usr/bin/add-apt-repository ]; then - sudo apt-get -y update - echo "Adding add-apt-repository..." - sudo apt-get -y install software-properties-common -fi - -# Install Ansible and its dependencies if not installed. -if [ ! -f /usr/bin/ansible ]; then - echo "Adding Ansible repository..." - sudo apt-add-repository -y ppa:ansible/ansible - echo "Updating system..." - sudo apt-get -y update - echo "Installing Ansible..." - sudo apt-get -y install ansible -fi - -if [ ! -d ${ANSIBLE_PATH}/vendor ]; then - echo "Running Ansible Galaxy install" - ansible-galaxy install -r ${ANSIBLE_PATH}/requirements.yml -p ${ANSIBLE_PATH}/vendor/roles -fi - -echo "Running Ansible Playbooks" -cd ${ANSIBLE_PATH}/ -ansible-playbook dev.yml -e vagrant_version=$1 diff --git a/xdebug-tunnel.yml b/xdebug-tunnel.yml new file mode 100644 index 0000000000..355a334b06 --- /dev/null +++ b/xdebug-tunnel.yml @@ -0,0 +1,18 @@ +--- +- name: Test Connection and Determine Remote User + hosts: "{{ xdebug_tunnel_inventory_host }}" + gather_facts: false + roles: + - { role: connection, tags: [connection, always] } + +- name: Enable or Disable Xdebug and SSH Tunnel + hosts: "{{ xdebug_tunnel_inventory_host }}" + become: yes + roles: + - { role: xdebug, tags: [xdebug] } + - { role: xdebug-tunnel, tags: [xdebug-tunnel] } + handlers: + - name: reload php-fpm + service: + name: php7.1-fpm + state: reloaded