├── .gitignore ├── .travis.provision.bin ├── .travis.vagrant.bin ├── .travis.yml ├── CHANGELOG.md ├── FAQ.md ├── LICENSE ├── README.md ├── bin ├── import ├── mock └── test ├── bower.json ├── composer.json ├── composer.lock ├── docs ├── REF-cap-tasks.md ├── REF-caveats-constants.md ├── REF-generator-prompts.md ├── REF-groupvar-overrides.md ├── REF-snapshots-config.md ├── TUTORIAL-CLONE.md ├── TUTORIAL-IMPORT.md ├── TUTORIAL-MOVE.md ├── TUTORIAL-NEW.md ├── TUTORIAL-UPGRADE.md ├── UPGRADE-FAQ.md ├── cambrian-install.gif ├── cambrian-menu.png ├── generate.gif └── wp-install.png ├── lib ├── ansible │ ├── lookup_plugins │ │ ├── ansible1_nested_dict.py │ │ ├── ansible2_nested_dict.py │ │ └── nested_dict.py │ └── roles │ │ ├── apache-prefork │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── prefork.conf │ │ ├── apache │ │ ├── files │ │ │ └── logrotate-apache │ │ ├── handlers │ │ │ └── main.yml │ │ ├── library │ │ │ └── restore │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ │ ├── canonical │ │ │ └── virtualhost │ │ └── vars │ │ │ └── main.yml │ │ ├── awstats │ │ └── tasks │ │ │ ├── main.yml │ │ │ └── vagrant.yml │ │ ├── cleanup │ │ ├── files │ │ │ ├── update.sh │ │ │ └── varnish-health.sh │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ │ └── evolution-init-d │ │ └── vars │ │ │ └── main.yml │ │ ├── common │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── logrotate │ │ ├── tasks │ │ │ ├── main.yml │ │ │ └── swap.yml │ │ ├── templates │ │ │ └── swapsize.j2 │ │ └── vars │ │ │ └── main.yml │ │ ├── debug │ │ ├── tasks │ │ │ └── main.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── firewall │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ ├── init.d │ │ │ │ └── iptables-persistent │ │ │ └── iptables │ │ │ │ ├── rules.v4 │ │ │ │ └── rules.v6 │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ │ ├── default │ │ │ │ └── iptables-persistent.conf │ │ │ └── fail2ban │ │ │ │ └── jail.local │ │ └── vars │ │ │ └── main.yml │ │ ├── mail │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── aliases │ │ ├── mysql │ │ ├── tasks │ │ │ └── main.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── newrelic │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ │ ├── node │ │ ├── tasks │ │ │ └── main.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── php-hardened │ │ ├── tasks │ │ │ ├── build.yml │ │ │ ├── main.yml │ │ │ └── suhosin.yml │ │ ├── templates │ │ │ └── suhosin.ini.j2 │ │ └── vars │ │ │ └── main.yml │ │ ├── php │ │ ├── tasks │ │ │ ├── main.yml │ │ │ ├── php5-install.yml │ │ │ ├── php5-uninstall.yml │ │ │ ├── php7-install.yml │ │ │ └── php7-uninstall.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── pound │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ ├── templates │ │ │ └── pound.cfg │ │ └── vars │ │ │ └── main.yml │ │ ├── prepare │ │ └── tasks │ │ │ └── main.yml │ │ ├── snapshots │ │ ├── files │ │ │ └── backup.py │ │ ├── tasks │ │ │ └── main.yml │ │ └── vars │ │ │ └── main.yml │ │ ├── user │ │ └── tasks │ │ │ └── main.yml │ │ ├── varnish │ │ ├── files │ │ │ ├── etc-default-varnish │ │ │ ├── etc-varnish │ │ │ │ ├── conf.d │ │ │ │ │ ├── fetch │ │ │ │ │ │ └── wordpress.vcl │ │ │ │ │ └── receive │ │ │ │ │ │ └── wordpress.vcl │ │ │ │ ├── custom.acl.vcl │ │ │ │ ├── custom.backend.vcl │ │ │ │ └── production.vcl │ │ │ └── remoteip.conf │ │ ├── handlers │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── vars │ │ │ └── main.yml │ │ └── wp-cli │ │ └── tasks │ │ └── main.yml ├── capistrano │ ├── bootstrap.rake │ └── tasks │ │ ├── db.rake │ │ ├── down.rake │ │ ├── files.rake │ │ ├── provision.rake │ │ ├── server.rake │ │ ├── snapshot.rake │ │ ├── ssh.rake │ │ ├── up.rake │ │ ├── update.rake │ │ ├── util.rake │ │ └── wp.rake └── yeoman │ ├── bin │ └── postinstall │ ├── index.js │ ├── templates │ ├── Capfile │ ├── Evolution.php │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── Vagrantfile │ ├── _bowerrc │ ├── _editorconfig │ ├── _gitignore │ ├── ansible.cfg │ ├── bin │ │ └── import │ ├── bower.json │ ├── lib │ │ ├── ansible │ │ │ ├── files │ │ │ │ ├── ssh │ │ │ │ │ └── .gitkeep │ │ │ │ └── ssl │ │ │ │ │ ├── .gitkeep │ │ │ │ │ ├── local.cfg │ │ │ │ │ ├── production.cfg │ │ │ │ │ └── staging.cfg │ │ │ ├── galaxy.yml │ │ │ ├── group_vars │ │ │ │ └── all │ │ │ ├── hosts │ │ │ ├── provision.yml │ │ │ └── user.yml │ │ └── capistrano │ │ │ ├── deploy.rb │ │ │ ├── deploy │ │ │ ├── local.rb │ │ │ ├── production.rb │ │ │ └── staging.rb │ │ │ └── tasks │ │ │ └── _gitkeep │ └── web │ │ ├── _htaccess │ │ ├── index.php │ │ ├── no_robots.txt │ │ ├── robots.txt │ │ ├── wp-config.php │ │ └── wp-content │ │ ├── mu-plugins │ │ └── _gitkeep │ │ ├── plugins │ │ └── _gitkeep │ │ ├── themes │ │ └── _gitkeep │ │ └── uploads │ │ └── _gitkeep │ └── welcome.txt ├── package.json ├── phpunit.xml └── test ├── README.md ├── functional ├── 00-firewall.js ├── 01-install.js ├── 02-wp.rewrite.structure.js ├── 03-evolve.provision.js ├── 04-deploy.js ├── 05-evolve.db.up.js ├── 06-evolve.files.up.js ├── 07-sni.js ├── 08-robots.js ├── varnish │ └── cache.js └── wordpress │ └── redirect.js └── unit └── wordpress └── EvolutionTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | temp/ 3 | test/temp/ 4 | vendor/ 5 | composer.phar 6 | *.pyc 7 | 8 | .vimrc 9 | 10 | -------------------------------------------------------------------------------- /.travis.provision.bin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Must be run as root... 4 | if [[ $EUID -ne 0 ]]; then 5 | echo "This script must be run root:" 6 | echo " \$ sudo $0" 7 | exit 1 8 | fi 9 | 10 | # Run provisioning playbook 11 | (cd /vagrant && ansible-playbook lib/ansible/provision.yml --connection=local -e stage=local) 12 | -------------------------------------------------------------------------------- /.travis.vagrant.bin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TRIGGERED= 4 | 5 | for ARG in "$@"; do 6 | if [[ ! -z "${TRIGGERED}" ]]; then 7 | eval ${ARG} 8 | exit $? 9 | fi 10 | 11 | if [[ "${ARG}" == '-c' ]]; then 12 | TRIGGERED=1 13 | fi 14 | done 15 | 16 | exit 0 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: generic 4 | addons: 5 | hosts: 6 | - example.com 7 | - local.example.com 8 | - production.example.com 9 | - www.example.com 10 | before_install: 11 | # node setup (for yeoman/bower) 12 | - nvm install 0.10 13 | - node --version 14 | - npm --version 15 | - nvm --version 16 | - npm install -g bower 17 | - npm install 18 | # ruby setup (for capistrano) 19 | - rvm use 1.9.3 20 | - gem install bundler 21 | - ruby --version 22 | - rvm --version 23 | - gem --version 24 | - bundle --version 25 | # python setup (for ansible) 26 | - sudo pip install ansible==1.9.4 27 | - python --version 28 | - pip --version 29 | - ansible --version 30 | # mock up test project, and move to /vagrant for end-user tests 31 | - $PWD/bin/mock 32 | - sudo mv $PWD/temp /vagrant 33 | - ln -s /vagrant $PWD/temp 34 | # copy our local evolution-wordpress as bower dependency 35 | - rm -rf /vagrant/bower_components/evolution-wordpress 36 | - cp -RP $PWD /vagrant/bower_components/evolution-wordpress/ 37 | # invoke local provision (as default user with elevated permissions) 38 | - sudo cp $PWD/.travis.provision.bin $PWD/bin/provision 39 | - sudo chmod +x $PWD/bin/provision 40 | - sudo $PWD/bin/provision 41 | - sudo chown -Rf $USER:$USER $HOME/.ansible 42 | # mock vagrant binary (for parity with local/remote stages) 43 | - sudo cp $PWD/.travis.vagrant.bin /bin/vagrant 44 | - sudo chmod +x /bin/vagrant 45 | # ensure known ssh fingerprints for remote stages 46 | - ssh-keyscan -H production.example.com >> $HOME/.ssh/known_hosts 47 | - ssh-keyscan -H www.example.com >> $HOME/.ssh/known_hosts 48 | - ssh-keyscan -H example.com >> $HOME/.ssh/known_hosts 49 | # install composer (for phpunit) 50 | - curl -sS https://getcomposer.org/installer | php 51 | install: 52 | - echo "Install Composer dependencies" 53 | - php composer.phar install --dev 54 | script: 55 | - ./vendor/bin/phpunit 56 | - npm test 57 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | This document lists commonly encountered issues, their likely causes, and likely solutions. 4 | 5 | ### Error establishing a database connection 6 | 7 | Is the MySQL database not running? Restart it with `sudo service mysql restart` 8 | 9 | Is `DB_HOST` in your wordpress configuration set to `localhost`? If so, there may be a mysql local socket issue...set it instead to `127.0.0.1` to use TCP/IP, and [see the documentation behind this known issue](http://php.net/mysql_connect#refsect1-function.mysql-connect-notes) 10 | 11 | ### Can't connect to local MySQL server through socket 12 | 13 | Is the socket file missing or inaccessible? Check with `ls -al /var/run/mysqld/*.sock` 14 | 15 | Is the MySQL database not running? Restart it with `sudo service mysql restart` 16 | 17 | ### Local stage hangs or never resolves in the browser 18 | 19 | This is a [known issue](https://github.com/evolution/wordpress/issues/74) related to vagrant hostmanager not properly cleaning up `/etc/hosts` entries for destroyed virtual machines. Find and remove any hostfile entries related to your domain, and it should resolve your issue. 20 | 21 | ### Capistrano fails with "fingerprint" or "host key verification" error 22 | 23 | ``` 24 | $ bundle exec cap local evolve:restart 25 | INFO[0223320c] Running /usr/bin/env sudo service evolution-wordpress restart on local.mytestsite.net 26 | DEBUG[0223320c] Command: /usr/bin/env sudo service evolution-wordpress restart 27 | cap aborted! 28 | SSHKit::Runner::ExecuteError: Exception while executing on host local.mytestsite.net: fingerprint bd:ae:df:28:36:37:0b:6c:3c:60:57:09:2b:af:5f:5d does not match for "local.mytestsite.net,192.168.12.345" 29 | ``` 30 | 31 | This is indicative of a known server's host key changing, most commonly after rebuilding a remote server or recreating your local server in vagrant. 32 | 33 | > Note: If you have made no such changes, you _might_ be the subject of [a MITM attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). 34 | 35 | First, we need to find the existing host key from your `known_hosts` file. 36 | 37 | ``` 38 | $ cat ~/.ssh/known_hosts | grep -n local.mytestsite.net 39 | 207:local.mytestsite.net,192.168.12.345 ssh-rsa AAAAB3N... 40 | ``` 41 | 42 | The output above indicates that the outdated host key is on line **207**, which you can delete with your preferred editor, or in-place with `sed`: 43 | 44 | ``` 45 | $ sed -i -e '207d' ~/.ssh/known_hosts 46 | ``` 47 | 48 | Finally, you'll need to manually start an ssh connection to said server, and accept the new host key. 49 | 50 | > Note you don't necessarily have to _complete_ authentication, just answer yes to the prompt 51 | 52 | ``` 53 | $ ssh -i lib/ansible/files/ssh/id_rsa deploy@local.mytestsite.net 54 | The authenticity of host 'local.mytestsite.net (192.168.12.345)' can't be established. 55 | RSA key fingerprint is bd:ae:df:28:36:37:0b:6c:3c:60:57:09:2b:af:5f:5d. 56 | Are you sure you want to continue connecting (yes/no)? 57 | ``` 58 | 59 | ### SSH Error "unix_listener...too long for Unix domain socket" 60 | 61 | This is a [known issue](https://github.com/ansible/ansible/issues/11536#issuecomment-153030743) with openssh and ansible, evidently usually seen on OSX El Capitan. It is ideally remedied by the **control_path** in `ansible.cfg` that evolution generates (as of v1.4.1). 62 | 63 | If a custom control path is already set in your `ansible.cfg` and you still encounter this issue, you can work around this with a shorter control path, eg using `%C` (if your version of ssh supports it): 64 | 65 | ``` 66 | [ssh_connection] 67 | control_path = %(directory)s/%%C 68 | ``` 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Eric Clemmons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Evolution WordPress 2 | 3 | [![Build Status](https://travis-ci.org/evolution/wordpress.svg)](https://travis-ci.org/evolution/wordpress) 4 | [![Dependencies](https://david-dm.org/evolution/wordpress.svg)](https://david-dm.org/evolution/wordpress) 5 | [![devDependencies](https://david-dm.org/evolution/wordpress/dev-status.svg)](https://david-dm.org/evolution/wordpress#info=devDependencies&view=table) 6 | 7 | > Rapidly create, develop, & deploy WordPress across multiple environments. 8 | > ![Generating a site](./docs/generate.gif) 9 | 10 | Evolution lets you generate an entirely versioned, multi-environment Wordpress site in under a minute! 11 | 12 | ### Features 13 | 14 | * Built on Ubuntu Linux 14.04 15 | * [Vagrant](https://www.vagrantup.com/) server for local development 16 | * Automated [Ansible](http://www.ansible.com/) provisioning 17 | * Automated [Capistrano](http://capistranorb.com/) deployment 18 | * Interval and on-demand backups to the cloud provider of your choice, and on-demand restore 19 | * Passwordless login over SSH 20 | * Secure HTTPS encryption 21 | * Server-side [Varnish](https://www.varnish-cache.org/) caching 22 | * Preconfigured [iptables](http://www.netfilter.org/projects/iptables/) firewall 23 | * Performance tuned [Apache](http://httpd.apache.org/) webserver 24 | * [Postfix](http://www.postfix.org/) mail server 25 | 26 | ## Project Status 27 | 28 | Evolution is _largely_ stable and usable at this point, but features are still being added and bugs being fixed. This documentation is also a work in progress. 29 | 30 | ## Quick Start 31 | 32 | Evolution is intended for use in a POSIX environment, such as Linux or Mac OS. Windows is not officially supported, but _may_ be possible with a POSIX subsystem like [Cygwin](https://www.cygwin.com/). 33 | 34 | ### Pre-requisites 35 | 36 | You will need: 37 | 38 | * [Vagrant](https://www.vagrantup.com/downloads.html) 1.8+ 39 | * [VirtualBox](https://www.virtualbox.org/wiki/Downloads) 5+ 40 | * [Hostmanager for Vagrant](https://github.com/smdahlen/vagrant-hostmanager#installation) 41 | * [npm](https://docs.npmjs.com/getting-started/installing-node) 42 | * [Bundler](http://bundler.io/) 43 | * [Ansible](http://docs.ansible.com/intro_installation.html) 2.0+ 44 | * [sshpass](https://gist.github.com/arunoda/7790979) 45 | 46 | You can then use npm to install Bower and the Yeoman generator: 47 | 48 | ``` 49 | npm install -g bower yo generator-evolve 50 | ``` 51 | 52 | ### Common Workflows 53 | 54 | * [Generating a new site](./docs/TUTORIAL-NEW.md) 55 | * [Bringing up an existing Evolution site](./docs/TUTORIAL-CLONE.md) 56 | * [Regenerating an existing Evolution site](./docs/TUTORIAL-UPGRADE.md) 57 | * [Rebuilding an existing Evolution server](./docs/TUTORIAL-MOVE.md) 58 | * [Upgrading from an existing Genesis site](./docs/UPGRADE-FAQ.md) 59 | * [Importing a non Evolution site](./docs/TUTORIAL-IMPORT.md) 60 | * [Caveats for developing themes and plugins](./docs/REF-caveats-constants.md) 61 | * [Common variable overrides](./docs/REF-groupvar-overrides.md) 62 | 63 | ## Managing Remote Environments 64 | 65 | Evolution exposes several commands via Capistrano for managing and supporting your remote environments. 66 | 67 | You can sync the database and uploaded files all at once...as well as separately: 68 | 69 | ``` 70 | bundle exec cap staging evolve:up 71 | bundle exec cap staging evolve:up:db 72 | bundle exec cap staging evolve:up:files 73 | ``` 74 | 75 | You can SSH directly to the remote server, without username or password prompts: 76 | 77 | ``` 78 | bundle exec cap staging evolve:ssh 79 | ``` 80 | 81 | You can remotely stop and start services, or even reboot the server: 82 | 83 | ``` 84 | bundle exec cap staging evolve:stop 85 | bundle exec cap staging evolve:start 86 | bundle exec cap staging evolve:restart 87 | bundle exec cap staging evolve:reboot 88 | ``` 89 | 90 | You can even remotely view logs: 91 | 92 | ``` 93 | bundle exec cap staging evolve:logs:apache:access 94 | bundle exec cap staging evolve:logs:apache:error 95 | bundle exec cap staging evolve:logs:varnish 96 | bundle exec cap staging evolve:logs:pound 97 | bundle exec cap staging evolve:logs:evolution 98 | ``` 99 | 100 | These and more can be found in the [Capistrano tasks reference](./docs/REF-cap-tasks.md). 101 | 102 | ## Troubleshooting 103 | 104 | If you've encountered a problem, there may already be a solution in the [Frequently Asked Questions](./FAQ.md). 105 | 106 | Failing that, check the [Github issues](https://github.com/evolution/wordpress/issues) and [pull requests](https://github.com/evolution/wordpress/pulls) to see if someone has already encountered your same problem. If no one has, then feel free to [file a new issue](https://github.com/evolution/wordpress/issues/new). 107 | 108 | ## Developing 109 | 110 | If you'd like to help develop or test new features for Evolution, it's relatively simple to do so! 111 | 112 | First, you'll need to clone this repo, and checkout whatever branch you're wanting to test (or create a new branch and implement a feature within it): 113 | 114 | ``` 115 | EVOLUTION_TEST_FRAMEWORK_PATH=~/git/evolution-wordpress 116 | mkdir -p $EVOLUTION_TEST_FRAMEWORK_PATH 117 | git clone https://github.com/evolution/wordpress.git $EVOLUTION_TEST_FRAMEWORK_PATH 118 | cd $EVOLUTION_TEST_FRAMEWORK_PATH 119 | git checkout -b some-existing-feature-from-origin 120 | ``` 121 | 122 | Next, go to the repo of the evolution site with which you intend to test the feature, and invoke the generator with the `framework-path` argument: 123 | 124 | ``` 125 | cd ~/git/my-testing-site.com 126 | yo evolve wordpress --framework-path=$EVOLUTION_TEST_FRAMEWORK_PATH 127 | ``` 128 | 129 | Now, this site should be generated from the feature branch in question! 130 | -------------------------------------------------------------------------------- /bin/mock: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs-extra'); 4 | var exec = require('child_process').exec; 5 | var helpers = require('yeoman-generator').test; 6 | var path = require('path'); 7 | 8 | var outputDir = path.join(__dirname, '..', 'temp'); 9 | var privatePath = path.join(outputDir, '/lib/ansible/files/ssh/id_rsa'); 10 | var privateKey = fs.existsSync(privatePath) ? fs.readFileSync(privatePath) : null; 11 | var publicPath = path.join(outputDir, '/lib/ansible/files/ssh/id_rsa.pub'); 12 | var publicKey = fs.existsSync(publicPath) ? fs.readFileSync(publicPath) : null; 13 | var certPath = path.join(outputDir, '/lib/ansible/files/ssl/example.com.pem'); 14 | var cert = fs.existsSync(certPath) ? fs.readFileSync(certPath) : null; 15 | 16 | var WordpressGenerator = require('../lib/yeoman'); 17 | 18 | helpers.testDirectory(outputDir, function(err) { 19 | if (err) { 20 | throw err; 21 | } 22 | 23 | console.log('Mocking in `%s`', outputDir); 24 | var isCI = (process.env.CI || process.env.TRAVIS); 25 | 26 | if (isCI) { 27 | var prep = [ 28 | 'git init ' + outputDir, 29 | ]; 30 | } 31 | else { 32 | var prep = [ 33 | 'git clone git@github.com:evolution/wordpress-example.git ' + outputDir, 34 | 'cd ' + outputDir, 35 | 'git rm -rf .', 36 | ]; 37 | } 38 | 39 | console.log('Executing `%s`', prep.join(' && ')); 40 | exec(prep.join(' && '), function(err, stdout, stderr) { 41 | if (err) { 42 | throw err; 43 | } 44 | 45 | var app = helpers.createGenerator('evolution-wordpress:app', [ 46 | [WordpressGenerator, 'evolution-wordpress:app'] 47 | ]); 48 | 49 | app.options['skip-install'] = true; 50 | app.options['dev'] = !isCI; 51 | 52 | helpers.mockPrompt(app, { 53 | private: false, 54 | ssl: true, 55 | name: 'Example.com', 56 | domain: 'example.com', 57 | www: false, 58 | roles: WordpressGenerator.roles.map(function(role) { return role.value; }), 59 | ip: '192.168.137.137', 60 | DB_NAME: 'example_db', 61 | DB_USER: 'example_user', 62 | DB_PASSWORD: 'example', 63 | DB_HOST: '127.0.0.1', 64 | prefix: 'wp_', 65 | }); 66 | 67 | app.run(function() { 68 | if (privateKey) { 69 | fs.writeFileSync(privatePath, privateKey); 70 | } 71 | 72 | if (publicKey) { 73 | fs.writeFileSync(publicPath, publicKey); 74 | } 75 | 76 | if (cert) { 77 | fs.writeFileSync(certPath, cert); 78 | } 79 | 80 | fs.appendFileSync(outputDir + '/lib/capistrano/deploy.rb', [ 81 | '', 82 | '# Added for testing purposes', 83 | 'set :repo_url, ' + ( 84 | isCI 85 | ? '"file:///vagrant/.git"' 86 | : '"https://github.com/evolution/wordpress-example.git"' 87 | ), 88 | '', 89 | ].join('\n')); 90 | 91 | var vagrantFile = fs.readFileSync(outputDir + '/Vagrantfile', 'utf8') 92 | .replace( 93 | new RegExp('(# Static IP)'), 94 | [ 95 | '# Additional host names for testing', 96 | ' box.hostmanager.aliases = ["example.com", "production.example.com", "www.example.com"]', 97 | '', 98 | ' $1', 99 | ].join('\n') 100 | ) 101 | .replace( 102 | new RegExp('(# Remount)'), 103 | [ 104 | '# Mount library for testing', 105 | ' box.vm.synced_folder "../", "/wordpress", :nfs => true', 106 | '', 107 | ' $1', 108 | ].join('\n') 109 | ) 110 | ; 111 | 112 | fs.writeFileSync(outputDir + '/Vagrantfile', vagrantFile); 113 | 114 | var wrap = [ 115 | 'git add .', 116 | ]; 117 | 118 | if (isCI) { 119 | Array.prototype.push.apply(wrap, [ 120 | 'git commit -m "Something to deploy"', 121 | 'git remote add origin ' + outputDir + '/.git', 122 | ]); 123 | } 124 | 125 | console.log('Executing `%s`', wrap.join(' && ')); 126 | exec(wrap.join(' && '), { cwd: outputDir }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var glob = require('glob'); 4 | var Mocha = require('mocha'); 5 | var path = require('path'); 6 | 7 | var mocha = new Mocha(); 8 | var testDir = path.join(__dirname, '..', 'test'); 9 | 10 | glob.sync('**/*.js', { 11 | cwd: testDir, 12 | nosort: true, 13 | }).forEach(function(file) { 14 | mocha.addFile(path.join(testDir, file)); 15 | }); 16 | 17 | mocha 18 | .bail(true) 19 | .reporter('spec') 20 | .timeout(0) 21 | .ui('bdd') 22 | .run(process.exit) 23 | ; 24 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evolution-wordpress", 3 | "version": "1.10.2", 4 | "homepage": "https://github.com/evolution/wordpress", 5 | "authors": [ 6 | "Eric Clemmons " 7 | ], 8 | "description": "Libraries for a multi-staged WordPress workflow with Vagrant", 9 | "keywords": [ 10 | "vagrant", 11 | "wordpress", 12 | "genesis", 13 | "evolution", 14 | "cap", 15 | "capistrano", 16 | "deploy" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evolution/wordpress", 3 | "description": "", 4 | "autoload": { 5 | "psr-0": { 6 | "Evolution": "temp" 7 | } 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "4.1.*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/REF-caveats-constants.md: -------------------------------------------------------------------------------- 1 | # Caveats for developing themes and plugins in Evolution 2 | 3 | When developing themes or plugins yourself, it's important that you understand how Evolution handles the _core wordpress code_. 4 | 5 | Traditionally in a wordpress site, _your_ code would be mixed in with wordpress' core files, and any time you upgrade wordpress, you'd see a flood of file changes and deletions...all of which need to be committed to version control. 6 | 7 | Evolution takes a different approach, and _isolates_ the wordpress core files into the `web/wp/` directory. Because these files are (1) dynamically pulled in with bower and (2) ignored by git, the core files are _never versioned_ and **should never have to be modified** by you. 8 | 9 | > **Sidenote:** This means that, if you upgrade Wordpress through the built-in dashboard, you should either (a) [regenerate your site](./TUTORIAL-UPGRADE.md) or (b) manually update the wordpress version in your `bower.json` file. 10 | > 11 | > Failing to do this could cause a subsequent deployment to accidentally _downgrade_ your current wordpress installation. 12 | 13 | Any themes, plugins, and uploaded content are stored separately in `web/wp-content`: 14 | 15 | ``` 16 | . <- project root 17 | └── web 18 | ├── wp <- wordpress core files, managed by bower and git-ignored 19 | └── wp-content 20 | ├── plugins <- plugins get installed here 21 | ├── themes <- themes get installed here 22 | └── uploads <- uploaded content gets saved here 23 | ``` 24 | 25 | Now, what does all of this mean for _developing_ your own themes and plugins? 26 | 27 | ### Filesystem paths 28 | 29 | When you work directly with the filesystem, or include php files, be aware of the separate directories noted above, and _use path constants_ as often as possible: 30 | 31 | * `ABSPATH` - This is the filesystem path to the (git-ignored and bower-managed) core wordpress files. 32 | * `WP_CONTENT_DIR` - This is the filesystem path to installed themes/plugins, and stored upload content. **In most cases, this is the one to use in your theme and plugin code.** 33 | 34 | Most other constants defined by the core wordpress code (`WP_PLUGIN_DIR`, for example) are built on top of `WP_CONTENT_DIR`. 35 | 36 | ### Home vs site URLs 37 | 38 | The difference between home and site URLs is [thoroughly explained here](http://pressing-matters.io/using-wp_siteurl-and-wp_home-to-migrate-a-wordpress-site/), but the short version goes like this: 39 | 40 | * The home url is _where you want visitors to reach your blog_. 41 | * The site url is literally _where your wordpress core files are installed_. 42 | 43 | It may not seem like it, but **this is a very important distinction**. 44 | 45 | From within your themes and plugins, you should _always_ use the `WP_HOME` constant or [`home_url()`](http://codex.wordpress.org/Function_Reference/home_url) API function when linking to the user-facing site. -------------------------------------------------------------------------------- /docs/REF-generator-prompts.md: -------------------------------------------------------------------------------- 1 | # Generator prompts 2 | 3 | * [Private or Public](#private-or-public) 4 | * [SSL Enabled](#ssl-enabled) 5 | * [Overwrite existing SSH keys](#overwrite-existing-ssh-keys) 6 | * [Project name](#project-name) 7 | * [Domain name](#domain-name) 8 | * [WWW or bare domain](#www-or-bare-domain) 9 | * [New Relic license key](#new-relic-license-key) 10 | * [Datadog license key](#datadog-license-key) 11 | * [Optional features](#optional-features) 12 | * [Wordpress version](#wordpress-version) 13 | * [Wordpress automatic updates](#wordpress-automatic-updates) 14 | * [Wordpress table prefix](#wordpress-table-prefix) 15 | * [Database credentials](#database-credentials) 16 | * [Vagrant IP](#vagrant-ip) 17 | 18 | --- 19 | 20 | ### Private or Public 21 | 22 | If you opt for a *public* project, the generator will configure your [`.gitignore`](http://git-scm.com/docs/gitignore) to prevent ssh keys and ssl certificates from being versioned. 23 | 24 | **This is ideal if you plan to open-source your site on a free public repository (we recommend Github for this).** 25 | 26 | If you opt for *private*, it will configure your [`bower.json`](https://github.com/bower/bower.json-spec#private) to prevent publication through Bower, but *your keys and certificates will be versioned*. 27 | 28 | **In this case, you should use a paid [private repository on Github](https://help.github.com/articles/making-a-public-repository-private/), or a free [private repository on Gitlab](http://doc.gitlab.com/ce/gitlab-basics/create-project.html).** 29 | 30 | ### SSL Enabled 31 | 32 | Evolution supports SSL encryption through [Pound](http://www.apsis.ch/pound), and will generate self-signed certificates for you, if you enable this option. 33 | 34 | If you plan to make heavy use of SSL, you'll almost certainly want to purchase a commercial cert from a third party CA. See [the difference between self-signed and Certificate Authorities](http://stackoverflow.com/questions/292732/self-signed-ssl-cert-or-ca#answer-292784). 35 | 36 | On regeneration of an existing project (where SSL is already enabled), you will be prompted whether to overwrite your existing SSL certificates with new self-signed certs. **The default for this is always no.** 37 | 38 | ### Overwrite existing SSH keys 39 | 40 | On regeneration of an existing project, you will also be prompted whether to overwrite your existing SSH keys with a newly generated keypair. **The default for this is always no.** 41 | 42 | This comes in particularly handy when moving your project to a new server, or when you think an existing server has been compromised and you don't trust the old keys. 43 | 44 | ### Project name 45 | 46 | This is the name of your project, and defaults to the name of the working directory. 47 | 48 | ### Domain name 49 | 50 | This is the top level domain of your project, and defaults to the name of the working directory (if it's a valid domain name). 51 | 52 | ### WWW or bare domain 53 | 54 | This determines the canonical home of your site, whether under a `www.` subdomain or on the bare domain. 55 | 56 | A redirect is also created in `web/.htaccess` to send requests for your bare domain to the `www.` subdomain -- or vice versa. 57 | 58 | ### New Relic license key 59 | 60 | When provided a license key, this will automatically set up [server monitoring](http://newrelic.com/server-monitoring) and the [PHP agent](https://docs.newrelic.com/docs/agents/php-agent/getting-started/new-relic-php) through New Relic. **The default for this is to skip New Relic installation.** 61 | 62 | ### Datadog license key 63 | 64 | When provided a license key, this will automatically set up [the base agent](https://github.com/DataDog/ansible-datadog) for Datadog monitoring, via ansible-galaxy. **The default for this is to skip Datadog installation.** 65 | 66 | ### Optional features 67 | 68 | This allows you disable the following features, **all of which are enabled by default**: 69 | 70 | ##### apache-prefork 71 | 72 | This scales the settings for Apache's [prefork module](http://httpd.apache.org/docs/2.4/mod/prefork.html) based on available memory of the server being provisioned. 73 | 74 | These values can be manually overridden in `lib/ansible/group_vars/all`. 75 | 76 | ##### php-hardened 77 | 78 | This restricts features of PHP that are known security risks, and installs the [Suhosin](https://suhosin.org/) extension. 79 | 80 | ##### varnish 81 | 82 | This configures a high performance [Varnish](https://www.varnish-cache.org/) reverse-proxy cache in front of your Apache webserver. 83 | 84 | ##### mail 85 | 86 | This configures a [postfix](http://www.postfix.org/) mail server, so that Wordpress can generate outgoing email messages. 87 | 88 | ##### firewall 89 | 90 | This configures a simple [iptables](https://en.wikipedia.org/wiki/Iptables) firewall (blocking all ports but SSH, HTTP, and HTTPS), and basic intrusion protection via [Fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page). 91 | 92 | ##### debug 93 | 94 | This installs some command line utilities that are useful for monitoring processes and bandwidth. Namely [htop](http://hisham.hm/htop/) and [iftop](http://www.ex-parrot.com/pdw/iftop/). 95 | 96 | ### Wordpress version 97 | 98 | This determines which version of Wordpress will be fetched from Github and installed as a bower dependency. **The default for this is the latest tagged version.** 99 | 100 | ### Wordpress automatic updates 101 | 102 | This determines whether to, once daily, apply updates for wordpress core, cli, plugins, and themes. This feature is switched **off** by default, but you can choose to apply only _minor_ core releases (eg, `4.6.1` to `4.6.3`) or even _major_ releases (eg, `4.6.1` to `4.7.2`). 103 | 104 | Note that automatic updates are staggered across stages -- 12am for staging, 12:10am for prod, 12:20am for local -- in order to avoid simultaneous and conflicting commits pushed to git. 105 | 106 | **In order for automatic updates to persist back to the git origin, you will need deploy keys set up via your git provider with write permissions.** 107 | 108 | ### Wordpress table prefix 109 | 110 | This determines the prefix applied to Wordpress database tables. **The default for this is `wp_`.** 111 | 112 | ### Database credentials 113 | 114 | This determines the credentials used by Wordpress to connect to MySQL. 115 | 116 | **The defaults are:** 117 | 118 | * Database: `wordpress` 119 | * Username: `wordpress` 120 | * Password: randomly generated 121 | * Host: `127.0.0.1` 122 | 123 | ### Vagrant IP 124 | 125 | This determines the IP address used for your local vagrant vm. **The default is a randomly computed address.** 126 | -------------------------------------------------------------------------------- /docs/REF-groupvar-overrides.md: -------------------------------------------------------------------------------- 1 | # Group_var overrides 2 | 3 | This documents variables that can be overridden in your site's ansible group vars file (located in `lib/ansible/group_vars/all`) to change various evolution behaviors. 4 | 5 | * [PHP](#php) 6 | * [`php__memory_limit`](#php__memory_limit) 7 | * [`php__version_7`](#php__version_7) 8 | * [Apache](#apache) 9 | * [`apache__start_servers`](#apache__start_servers) 10 | * [`apache__min_spare_servers`](#apache__min_spare_servers) 11 | * [`apache__max_spare_servers`](#apache__max_spare_servers) 12 | * [`apache__max_clients`](#apache__max_clients) 13 | * [`apache__max_requests_per_child`](#apache__max_requests_per_child) 14 | * [Wordpress](#wordpress) 15 | * [`wordpress__xmlrpc_allow`](#wordpress__xmlrpc_allow) 16 | * [`wordpress__xmlrpc_whitelist`](#wordpress__xmlrpc_whitelist) 17 | * [IPTables](#iptables) 18 | * [`iptables__ipv6`](#iptables__ipv6) 19 | * [Fail2ban](#fail2ban) 20 | * [`fail2ban__whitelist`](#) 21 | * [`fail2ban__ban_time`](#) 22 | * [`fail2ban__notification_email`](#) 23 | * [`fail2ban__notify_on_ban`](#) 24 | * [Swapfile](#swapfile) 25 | * [`swap__path`](#swap__path) 26 | * [`swap__swappiness`](#swap__swappiness) 27 | * [`swap__vfs_cache_pressure`](#swap__vfs_cache_pressure) 28 | 29 | --- 30 | 31 | ## PHP 32 | 33 | ### `php__memory_limit` 34 | 35 | Sets PHP's [memory_limit](http://php.net/manual/en/ini.core.php#ini.memory-limit) directive (in megabytes): 36 | 37 | ```yml 38 | # Configure PHP with 4 gigabytes of memory 39 | php__memory_limit: 4096 40 | ``` 41 | 42 | By default, this is [calculated during provisioning based on the server's total available memory](https://github.com/evolution/wordpress/blob/0de3498380aaf4cfd79c7588be828ae6f401aa59/lib/ansible/roles/php/tasks/main.yml#L19-L23). 43 | 44 | ### `php__version_7` 45 | 46 | Triggers installation and use of PHP 7.1 from [a custom PPA](https://launchpad.net/~ondrej/+archive/ubuntu/php): 47 | 48 | ```yml 49 | # Install PHP 7.1 50 | php__version_7: true 51 | ``` 52 | 53 | By default, Ubuntu's provided packages for 5.5 are used. 54 | 55 | ## Apache 56 | 57 | Several overrides are available for the [MPM prefork module](https://httpd.apache.org/docs/2.4/mod/prefork.html) that Apache uses with PHP. By default, these prefork directives are [calculated during provisioning based on the server's available resources](https://github.com/evolution/wordpress/blob/0de3498380aaf4cfd79c7588be828ae6f401aa59/lib/ansible/roles/apache-prefork/templates/prefork.conf) 58 | 59 | ### `apache__start_servers` 60 | 61 | The number of child server processes created by Apache on startup. 62 | 63 | ### `apache__min_spare_servers` 64 | 65 | The minimum number of idle child server processes. 66 | 67 | ### `apache__max_spare_servers` 68 | 69 | The maximum number of idle child server processes. 70 | 71 | ### `apache__max_clients` 72 | 73 | The limit on the number of simultaneous requests that will be served. 74 | 75 | ### `apache__max_requests_per_child` 76 | 77 | The limit on the number of connections that an individual child server process will handle. 78 | 79 | ## Wordpress 80 | 81 | By default, access to Wordpress' XML-RPC functionality is unconditionally denied due to security concerns. There are two methods for overriding this, outlined below. 82 | 83 | ### `wordpress__xmlrpc_allow` 84 | 85 | A truthy value for this override allows unconditional access to Wordpress' XML-RPC: 86 | 87 | ```yml 88 | wordpress__xmlrpc_allow: true 89 | ``` 90 | 91 | This would generate the following in your virtual host configuration: 92 | 93 | ```apache 94 | # Enable XML-RPC unconditionally 95 | 96 | Order allow,deny 97 | Allow from all 98 | 99 | ``` 100 | 101 | ### `wordpress__xmlrpc_whitelist` 102 | 103 | A dictionary (key-value hash) for this override allows conditional access by way of [Apache's SetEnvIf](https://httpd.apache.org/docs/2.4/mod/mod_setenvif.html#setenvif) directive. 104 | 105 | The key is a regular expression match (that must be unique within the whitelist), and the value is an attribute that the expression would successfully match. This, for example, would allow XML-RPC requests from the local host: 106 | 107 | ```yml 108 | wordpress__xmlrpc_whitelist: 109 | '^127[.]0[.]0[.]1$': 'Remote_Addr' 110 | ``` 111 | 112 | This would generate the following in your virtual host configuration: 113 | 114 | ```apache 115 | # Enable XML-RPC for the following SetEnvIf conditions 116 | SetEnvIf Remote_Addr "^127[.]0[.]0[.]1$" allow_wp_xmlrpc 117 | 118 | Order deny,allow 119 | Deny from all 120 | Allow from env=allow_wp_xmlrpc 121 | 122 | ``` 123 | -------------------------------------------------------------------------------- /docs/REF-snapshots-config.md: -------------------------------------------------------------------------------- 1 | # Snapshots configuration 2 | 3 | * [Local vs cloud storage](#local-vs-cloud-storage) 4 | * [Storage method](#storage-method) 5 | * [Storage credentials](#storage-credentials) 6 | * [Storage container](#storage-container) 7 | * [Backup interval](#backup-interval) 8 | * [Backup retention](#backup-retention) 9 | 10 | --- 11 | 12 | > See also the snapshot-specific [cap tasks](./REF-cap-tasks.md). 13 | 14 | ### Local vs Cloud Storage 15 | 16 | Keeping backups on the local filesystem, while simpler to configure, does not make for a good disaster recovery scenario. To this end, Evolution now supports snapshots to a variety of cloud storage providers via [Apache Libcloud](https://libcloud.readthedocs.io/en/latest/). 17 | 18 | A commented out default configuration (for local storage) can be found in your ansible group vars (`lib/ansible/group_vars/all` within your project): 19 | 20 | ```yml 21 | # snapshots__method: local 22 | # snapshots__credentials: ~ 23 | # snapshots__container: /home/deploy/backup/production.example.com 24 | # snapshots__interval: 1d 25 | # snapshots__retention: { hours: ~, days: 1, weeks: 1, months: 1, years: 1 } 26 | # snapshots__retention_lag: true 27 | ``` 28 | 29 | To enable backups, uncomment _at least one_ of the above variables and reprovision your production remote. This will install the backup script and set up a cron job to run it regularly. 30 | 31 | > Note: The script is run every hour, but new backups are created and old backups removed only as often as your configured `interval`. 32 | 33 | Each backup consists of a sql dump of your database and a copy of your wordpress uploads directory, rolled into a gzipped tar archive. 34 | 35 | ### Storage Method 36 | 37 | The default is to backup to the local filesystem, but you can use [any storage provider supported by libcloud](https://libcloud.readthedocs.io/en/latest/storage/supported_providers.html#supported-methods-storage). Just plug in the given provider's constant (case insensitive): 38 | 39 | ```yml 40 | # Rackspace Cloud Files 41 | snapshots__method: cloudfiles 42 | # Google Cloud Storage 43 | snapshots__method: google_storage 44 | # Amazon Simple Storage Service 45 | snapshots__method: s3 46 | ``` 47 | 48 | ### Storage Credentials 49 | 50 | For all methods but local storage, you _must_ supply authentication credentials for your cloud provider. You can do so with positional or named parameters, [as documented in the libcloud API](https://libcloud.readthedocs.io/en/latest/storage/api.html#libcloud.storage.base.StorageDriver): 51 | 52 | ```yml 53 | # S3 54 | snapshots__credentials: 55 | - "My AWS api key" 56 | - "My AWS secret key" 57 | # Rackspace 58 | snapshots__credentials: 59 | key: "My Rackspace username" 60 | secret: "My Rackspace api key" 61 | ``` 62 | 63 | ### Storage Container 64 | 65 | For local storage, the `container` serves as a path to the directory in which all backups are stored. For all other storage methods, it serves as [a container in libcloud's terminology](https://libcloud.readthedocs.io/en/latest/storage/index.html#terminology), wherein backups are stored as objects within the given container. 66 | 67 | Typically, you would want to create a container (an S3 bucket, for example) in advance and then plug it into your group vars. That said, the backup script **will attempt to create the container if it does not already exist**. This may or may not succeed, depending on user permissions. 68 | 69 | ```yml 70 | # Defaults to "$stage.$domain"... 71 | snapshots__container: "local.example.com" 72 | # Use the below named S3 bucket 73 | snapshots__container: "My.S3.bucket.name" 74 | ``` 75 | 76 | ### Backup interval 77 | 78 | Evolution supports snapshot backups as often as _once an hour_, but the default is a more conservative _once a day_. 79 | 80 | The interval should be a number followed by a letter indicating the unit of time, in hours, days, weeks, months or years: 81 | 82 | ```yml 83 | # Every eight hours 84 | snapshots__interval: 8h 85 | # Every two days 86 | snapshots__interval: 2d 87 | # Every three weeks 88 | snapshots__interval: 3w 89 | # Every four months 90 | snapshots__interval: 4m 91 | # Every year 92 | snapshots__interval: 1y 93 | ``` 94 | 95 | ### Backup retention 96 | 97 | Evolution supports a retention policy for your backups, loosely based on the [grandfather-father-son scheme](https://en.wikipedia.org/wiki/Backup_rotation_scheme#Grandfather-father-son_backup). This allows you to keep a variety of older and newer backups, without filling up your storage. 98 | 99 | ```yml 100 | snapshots__retention: 101 | hours: ~ 102 | days: 1 103 | weeks: 1 104 | months: 1 105 | years: 1 106 | snapshots__retention_lag: true 107 | ``` 108 | 109 | The default retention policy (above) preserves one yearly, one monthly, one weekly, and one daily backup. The rest would be pruned over time, as more recent backups are created. 110 | 111 | > Note: Retention can be _deliberately kept one interval unit back from the current time_ with the `retention_lag` variable, in order to retain a backup of each period. (This is turned _on_ by default!) 112 | > 113 | > For example, with the default interval of once a day, you would create a new backup on Sunday (the start of a new week) and be left with a "yearly" (from January 1st), a "monthly" (from the 1st of the current month), a previous "weekly" (from the previous Sunday), and _possibly_ a previous daily from Saturday. 114 | > 115 | > This happens because the retention calculation would believe it is still Saturday (back one "interval" day), in order to preserve the previous weekly backup. **Turning off the retention lag** would cause the previous Sunday's "weekly" to be removed, leaving you with just the yearly, monthly, and today. 116 | 117 | This is best illustrated by running the backup script with the `--simulate` flag, which will display all backups that _would have been_ created and deleted for **one year ago from the current date and time**. (For brevity, only the retained backups are shown below.) 118 | 119 | With retention lag turned on: 120 | 121 | ``` 122 | created on deleted on retained for filename 123 | -------------------------- -------------------------- ----------------- ------------------------------------------------------- 124 | 2017-01-01 17:53:12.631052 176 days, 0:00:00 production.example.com-2017-01-01_17-53-12.631052.tgz 125 | 2017-06-01 17:53:12.631052 25 days, 0:00:00 production.example.com-2017-06-01_17-53-12.631052.tgz 126 | 2017-06-18 17:53:12.631052 8 days, 0:00:00 production.example.com-2017-06-18_17-53-12.631052.tgz 127 | 2017-06-24 17:53:12.631052 2 days, 0:00:00 production.example.com-2017-06-24_17-53-12.631052.tgz 128 | 2017-06-25 17:53:12.631052 1 day, 0:00:00 production.example.com-2017-06-25_17-53-12.631052.tgz 129 | ``` 130 | 131 | And with retention_lag turned _off_: 132 | 133 | ``` 134 | created on deleted on retained for filename 135 | -------------------------- -------------------------- ----------------- ------------------------------------------------------- 136 | 2017-01-01 17:53:17.027798 176 days, 0:00:00 production.example.com-2017-01-01_17-53-17.027798.tgz 137 | 2017-06-01 17:53:17.027798 25 days, 0:00:00 production.example.com-2017-06-01_17-53-17.027798.tgz 138 | 2017-06-25 17:53:17.027798 1 day, 0:00:00 production.example.com-2017-06-25_17-53-17.027798.tgz 139 | ``` 140 | -------------------------------------------------------------------------------- /docs/TUTORIAL-CLONE.md: -------------------------------------------------------------------------------- 1 | # Bringing up an existing Evolution site 2 | 3 | ### Cloning from the remote 4 | 5 | If you don't already have the site running locally (eg, someone else set it up), you will want to clone your own local copy: 6 | 7 | git clone git@yourremoteprovider.com:yourusername/Example.com.git ~/Example.com 8 | cd ~/Example.com 9 | 10 | ##### Sidenote: Extra step for public projects 11 | > Since public projects don't have ssh keys or ssl certificates versioned, you would have to acquire them from whoever set up the site initially, and manually put them in the appropriate directories: 12 | > * ssh keys go in: `lib/ansible/files/ssh/` 13 | > * ssl certs go in: `lib/ansible/files/ssl/` 14 | 15 | ### Installing dependencies 16 | 17 | During generation of a new or existing site, the generator does this step for you. Manually installing dependencies is still quite simple, however: 18 | 19 | bundle install 20 | bower install 21 | 22 | Note that bower will automatically pull in non-breaking changes from the latest patch version of Evolution (eg, `v1.0.15` to `v1.0.16`). 23 | 24 | ### Bringing up local 25 | 26 | You can start the local environment with vagrant: 27 | 28 | vagrant up 29 | 30 | At this point, you can sync down from a remote environment (staging in this case) any Wordpress configuration and content that already exists: 31 | 32 | bundle exec cap staging evolve:down 33 | 34 | ### What now? 35 | 36 | When collaborating on a site with others, be sure to pull down others' changes before commiting and pushing your own, or better yet use branches to keep incomplete features separate. 37 | -------------------------------------------------------------------------------- /docs/TUTORIAL-IMPORT.md: -------------------------------------------------------------------------------- 1 | # Importing a non Evolution site 2 | 3 | ### Out with the old 4 | 5 | From your existing wordpress site (must be 3.0 or higher), you'll need to install [the latest release](https://github.com/evolution/wordpress-plugin-cambrian/releases/latest) of the **Cambrian Explosion** plugin. For convenience, `cambrian.zip` is pre-packaged to drop right into the Wordpress admin. 6 | 7 | ![Installing Cambrian](./cambrian-install.gif) 8 | 9 | You'll also need to activate the plugin (for multisite, you should activate from whichever subsite you'll be exporting). There should now be a new item under the _Tools_ menu. 10 | 11 | ![Cambrian Menu Item](./cambrian-menu.png) 12 | 13 | Click the button and you should be presented with some output and eventually an export zip file. 14 | 15 | ### The blank slate 16 | 17 | You'll need to [follow the guide](./TUTORIAL-NEW.md) for generating a new site, but **stop once you reach the Wordpress install page** — any configuration will come after the import process. 18 | 19 | ### In with the new 20 | 21 | Run the import script in your newly generated site, provide the path to the export file when prompted, and follow any further prompts. 22 | 23 | ./bin/import 24 | 25 | Once complete, open your local site in the browser, which should have all the content, themes, and plugins from your old site. 26 | 27 | ##### Sidenote: Tidying up 28 | > Some activation, reconfiguration, and cleanup may be necessary depending on your existing plugins and themes. 29 | 30 | ### What now? 31 | 32 | From here, you can continue to 33 | [follow the guide](./TUTORIAL-NEW.md#useful-tools-for-all-stages) for preparing and deploying to remote environments. -------------------------------------------------------------------------------- /docs/TUTORIAL-MOVE.md: -------------------------------------------------------------------------------- 1 | # Moving to a new remote server 2 | 3 | ### What you'll need 4 | 5 | 1. A local stage with data synced down from your old remote stage 6 | * normally, easy as `bundle exec cap production evolve:down` 7 | 2. A fresh Ubuntu server, accessible via ssh 8 | 9 | ##### Sitenote: Moving from a compromised remote stage 10 | > If you are moving to a new server because the old one was hacked, you should either **use a backup made prior to the compromise**, or take specific action to [sanitize your data and remove any lingering malware](https://codex.wordpress.org/FAQ_My_site_was_hacked). 11 | > 12 | > You should also regenerate your site, **overwriting your old ssh keys and wordpress salts** in the process. Note this likely requires _reprovisioning all_ of your stages, afterward. 13 | > 14 | > Because Evolution encourages version control of your themes and plugins, you can detect and undo any unauthorized changes made to them with `git diff`. 15 | 16 | ### Setting up 17 | 18 | It's common to want your new stage provisioned, deployed, and tested before switching DNS over from the old stage. You can do this by updating the ansible inventory file and the relevant capistrano stage configuration. 19 | 20 | You'll need to replace the old DNS entry (eg, `production.example.com`) with either an ip address, a [hosts file entry](https://en.wikipedia.org/wiki/Hosts_(file)), or a new DNS record. For example sake, we'll use `new.example.com`. 21 | 22 | ```diff 23 | diff --git a/lib/ansible/hosts b/lib/ansible/hosts 24 | index c547d91..2810e62 100644 25 | --- a/lib/ansible/hosts 26 | +++ b/lib/ansible/hosts 27 | @@ -2,7 +2,7 @@ 28 | local.example stage=local 29 | 30 | [production] 31 | -production.example.com stage=production 32 | +new.example.com stage=production 33 | 34 | [staging] 35 | staging.example.com stage=staging 36 | diff --git a/lib/capistrano/deploy/production.rb b/lib/capistrano/deploy/production.rb 37 | index 6ea0020..24e23fa 100644 38 | --- a/lib/capistrano/deploy/production.rb 39 | +++ b/lib/capistrano/deploy/production.rb 40 | @@ -1,4 +1,4 @@ 41 | -server 'production.example.com', 42 | +server 'new.example.com', 43 | roles: %w{db web}, 44 | user: fetch(:user), 45 | ssh_options: fetch(:ssh_options) 46 | ``` 47 | 48 | Now, provision, deploy, and sync your new stage: 49 | 50 | ``` 51 | bundle exec cap production evolve:provision 52 | bundle exec cap production deploy 53 | bundle exec cap production evolve:up 54 | ``` 55 | 56 | ### Cleaning up 57 | 58 | Once you plug `new.example.com` into your browser and confirm it brings up your site as expected, you can switch over dns for `production.example.com` and revert the file changes above. 59 | 60 | Enjoy your new production environment! 61 | -------------------------------------------------------------------------------- /docs/TUTORIAL-UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Regenerating an existing Evolution site 2 | 3 | ### Why regenerate your site? 4 | 5 | Non-breaking changes to Evolution are added across **patch versions** (eg, `v1.0.15` to `v1.0.16`), and are automatically pulled down on `bower install` or deployment to a remote stage. 6 | 7 | Breaking changes are added across **minor versions** (eg `v1.0.16` to `v1.1.0`), and require running the generator against your existing site codebase 8 | 9 | ##### Sidenote: Regenerating means recreating 10 | > It's important to have your site already versioned in git and to have any recent changes already committed before regenerating. 11 | > 12 | > This is because running the generator may _recreate_ certain files, and git will show you _precisely_ what will have changed in your site. 13 | 14 | Bring up your local copy of the site, or [follow the guide](./TUTORIAL-ClONE.md#cloning-from-the-remote) if you don't already have one. 15 | 16 | ### Regenerating 17 | 18 | Now, run the generator and follow [the prompts](./REF-generator-prompts.md) -- it should pre-select the choices for which it was already configured, and install bundler and bower dependencies automatically: 19 | 20 | yo evolve wordpress 21 | git status 22 | 23 | Git will show you what, if anything, has changed. Add and commit them as necessary. 24 | 25 | ### Reprovisioning 26 | 27 | You should reboot and reprovision your local environment, in case anything in the ansible playbooks have changed. This can be done in a single step: 28 | 29 | vagrant reload --provision 30 | 31 | ##### Sidenote: Ansible and idempotence 32 | > The playbooks we use for provisioning are designed to be idempotent, meaning that we _should_ be able to provision a machine repeatedly and achieve the same end result. 33 | > 34 | > Should you ever find that reprovisioning fails, we encourage you to [file a Github issue](https://github.com/evolution/wordpress/issues/new) with the full ansible output (to help us diagnose and reproduce the problem). 35 | 36 | You should test your local site to ensure it is working properly, after which you can reprovision and redeploy your remote environments: 37 | 38 | bundle exec cap staging evolve:provision 39 | bundle exec cap staging deploy 40 | 41 | You _may_ also want to reboot the server: 42 | 43 | bundle exec cap staging evolve:reboot 44 | 45 | ### What now? 46 | 47 | From here, you can work on the local environment or sync up/down to remote environments as you normally would. 48 | -------------------------------------------------------------------------------- /docs/UPGRADE-FAQ.md: -------------------------------------------------------------------------------- 1 | # Upgrading from Genesis 2 | 3 | This is an overview of necessary cleanup and issues when regenerating a [Genesis WordPress](https://github.com/genesis/wordpress) site with Evolution. You should bring over any database and uploaded files [via the Cambrian export/import process](./TUTORIAL-upgrade.md). 4 | 5 | ### Removing Genesis' leftovers 6 | 7 | * Remove the old provisioning script: `rm bin/provision` 8 | * Remove leftover directories created by Genesis: 9 | * `rm -r deployment` 10 | * `rm -r provisioning` 11 | * Typically, any subdirectory you did not _explicitly_ create, other than `bin`, `lib`, and `web` 12 | * Remove all your old wordpress files and directories from `web`, except `wp-content` and `wp-config.php`: 13 | * `rm -r web/wp-{a,b,com,cr,i,l,m,s,t}*` 14 | 15 | ### Changing dependencies 16 | 17 | * If you've added any non-standard components to `bower.json`, Evolution will overwrite them. You will need to re-add them yourself. See precisely what's changed with: `git diff bower.json` 18 | * Capistrano was upgraded from 2 to 3, so the following files will have changed _drastically_: 19 | * `Capfile` 20 | * `Gemfile` 21 | * `Gemfile.lock` 22 | * Remove any possibly outdated dependencies, and then run your installs from scratch: 23 | * `rm -r ./{node_modules,bower_components}` 24 | * `npm install; bower install; bundle install` 25 | 26 | ### Resolving updates of existing files 27 | 28 | * Your `Vagrantfile` will be drastically different, and you may have to destroy and re-create your local vm 29 | * Any customizations you've made to your `.gitignore` will need to be re-added, [below a demarcated line](https://github.com/evolution/wordpress-example/blob/4fd6256fb8637f912e91f56a7f30cc486dd5a056/.gitignore#L57) 30 | * You'll see significant changes to `web/.htaccess`: 31 | * Forced redirects from `www.{domain}` to the bare domain (or vice versa) are standard in Evolution's htaccess 32 | * Most security + performance tweaks from Genesis' htaccess were moved to Apache's virtual host configuration 33 | * Any existing Genesis block should be _removed_ 34 | 35 | ### Wordpress snafus 36 | 37 | * Are all your links prefixed with `/wp/` and 404ing? Check that your themes and plugins use `home_url()` instead of `site_url()` 38 | -------------------------------------------------------------------------------- /docs/cambrian-install.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/docs/cambrian-install.gif -------------------------------------------------------------------------------- /docs/cambrian-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/docs/cambrian-menu.png -------------------------------------------------------------------------------- /docs/generate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/docs/generate.gif -------------------------------------------------------------------------------- /docs/wp-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/docs/wp-install.png -------------------------------------------------------------------------------- /lib/ansible/lookup_plugins/ansible1_nested_dict.py: -------------------------------------------------------------------------------- 1 | # (c) 2014, Evan Kaufman 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | 18 | from ansible.utils import safe_eval 19 | import ansible.utils as utils 20 | import ansible.errors as errors 21 | 22 | def flatten_nested_hash_to_list(terms, result=None, stack=None, level=None): 23 | result = [] if result is None else result 24 | stack = {} if stack is None else stack 25 | level = 0 if level is None else level 26 | 27 | for key, val in terms.iteritems(): 28 | stack['key_%s' % level] = key 29 | if type(val) is dict: 30 | flatten_nested_hash_to_list(terms=val, result=result, stack=stack.copy(), level=level+1) 31 | else: 32 | stack['value'] = val 33 | result.append(stack.copy()) 34 | return result 35 | 36 | class LookupModule(object): 37 | 38 | def __init__(self, basedir=None, **kwargs): 39 | self.basedir = basedir 40 | 41 | def run(self, terms, inject=None, **kwargs): 42 | terms = utils.listify_lookup_plugin_terms(terms, self.basedir, inject) 43 | 44 | if not isinstance(terms, dict): 45 | raise errors.AnsibleError("with_nested_dict expects a dict") 46 | 47 | return flatten_nested_hash_to_list(terms) 48 | -------------------------------------------------------------------------------- /lib/ansible/lookup_plugins/ansible2_nested_dict.py: -------------------------------------------------------------------------------- 1 | # (c) 2016, Evan Kaufman 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | from __future__ import (absolute_import, division, print_function) 18 | __metaclass__ = type 19 | 20 | from ansible.errors import AnsibleError 21 | from ansible.plugins.lookup import LookupBase 22 | 23 | """ 24 | Lookup plugin to flatten nested dictionaries for linear iteration 25 | ================================================================= 26 | 27 | For example, a collection of holidays nested by year, month, and day: 28 | 29 | { 30 | '2016': { 31 | 'January': { 32 | '1': 'New Years Day', 33 | '19': 'Martin Luther King Day', 34 | }, 35 | 'February': { 36 | '14': 'Valentines Day', 37 | '16': 'Presidents Day', 38 | }, 39 | ... 40 | }, 41 | ... 42 | } 43 | 44 | Would be flattened into an array of shallow dicts, with each original key denoted by its nesting level: 45 | 46 | [ 47 | { 'key_0': '2016', 'key_1': 'January', 'key_2': '1', 'value': 'New Years Day' }, 48 | { 'key_0': '2016', 'key_1': 'January', 'key_2': '19', 'value': 'Martin Luther King Day' }, 49 | { 'key_0': '2016', 'key_1': 'February', 'key_2': '14', 'value': 'Valentines Day' }, 50 | { 'key_0': '2016', 'key_1': 'February', 'key_2': '16', 'value': 'Presidents Day' }, 51 | ... 52 | ] 53 | 54 | The nested keys and value of each are then exposed to an iterative task: 55 | 56 | - name: "Remind us what holidays are this month" 57 | debug: 58 | msg: "Remember {{value}} on {{key_1}} {{'%02d'|format(key_2)}} in the year {{key_0}}" 59 | when: key_1 == current_month 60 | with_nested_dict: holidays_by_year_month_day 61 | 62 | """ 63 | class LookupModule(LookupBase): 64 | 65 | @staticmethod 66 | def _flatten_nested_hash_to_list(terms, result=None, stack=None, level=None): 67 | result = [] if result is None else result 68 | stack = {} if stack is None else stack 69 | level = 0 if level is None else level 70 | 71 | for key, val in terms.items(): 72 | stack['key_%s' % level] = key 73 | if type(val) is dict: 74 | LookupModule._flatten_nested_hash_to_list(terms=val, result=result, stack=stack.copy(), level=level+1) 75 | else: 76 | stack['value'] = val 77 | result.append(stack.copy()) 78 | return result 79 | 80 | def run(self, terms, variables=None, **kwargs): 81 | if not isinstance(terms, dict): 82 | raise AnsibleError("with_nested_dict expects a dict") 83 | 84 | return self._flatten_nested_hash_to_list(terms) 85 | -------------------------------------------------------------------------------- /lib/ansible/lookup_plugins/nested_dict.py: -------------------------------------------------------------------------------- 1 | # add current dir to import path 2 | import sys 3 | import os 4 | sys.path.append(os.path.dirname(__file__)) 5 | 6 | # import the right plugin for the right ansible major version 7 | from ansible import __version__ as ansible_version 8 | if ansible_version.startswith('2.'): 9 | from ansible2_nested_dict import LookupModule 10 | elif ansible_version.startswith('1.'): 11 | from ansible1_nested_dict import LookupModule 12 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache-prefork/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - php 4 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache-prefork/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create custom apache prefork config (2.2) 3 | template: src=prefork.conf dest=/etc/apache2/prefork.conf mode=0644 4 | when: apache_version.stdout == '2.2' 5 | become: true 6 | 7 | - name: Remove default apache prefork config (2.2) 8 | replace: dest=/etc/apache2/apache2.conf backup=yes regexp='^(?:#[^\n]+\n)*[^<]+?<\/IfModule>' replace='Include prefork.conf' validate='/usr/sbin/apache2ctl -f %s -t' 9 | when: apache_version.stdout == '2.2' 10 | become: true 11 | 12 | - name: Override apache prefork module config (2.4) 13 | template: src=prefork.conf dest=/etc/apache2/mods-available/mpm_prefork.conf backup=yes mode=0644 14 | when: apache_version.stdout == '2.4' 15 | become: true 16 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache-prefork/templates/prefork.conf: -------------------------------------------------------------------------------- 1 | {%- if apache__start_servers is defined -%} 2 | {% set _apache_start_servers = apache__start_servers %} 3 | {%- else -%} 4 | {% set _apache_start_servers = 10 if ansible_memtotal_mb >= 2000 else 3 %} 5 | {%- endif -%} 6 | 7 | {%- if apache__min_spare_servers is defined -%} 8 | {% set _apache_min_spare_servers = apache__min_spare_servers %} 9 | {%- else -%} 10 | {% set _apache_min_spare_servers = 8 if ansible_memtotal_mb >= 2000 else 3 %} 11 | {%- endif -%} 12 | 13 | {%- if apache__max_spare_servers is defined -%} 14 | {% set _apache_max_spare_servers = apache__max_spare_servers %} 15 | {%- else -%} 16 | {% set _apache_max_spare_servers = 15 if ansible_memtotal_mb >= 2000 else 3 %} 17 | {%- endif -%} 18 | 19 | {%- if apache__max_requests_per_child is defined -%} 20 | {% set _apache_max_requests_per_child = apache__max_requests_per_child %} 21 | {%- else -%} 22 | {% set _apache_max_requests_per_child = 1000 %} 23 | {%- endif -%} 24 | 25 | {%- if apache__max_clients is defined -%} 26 | {% set _apache_max_clients = apache__max_clients %} 27 | {%- else -%} 28 | {%- if ansible_memtotal_mb >= 2000 -%} 29 | {% set _apache_max_clients = 25 -%} 30 | {%- elif ansible_memtotal_mb >= 900 -%} 31 | {% set _apache_max_clients = 10 -%} 32 | {%- else -%} 33 | {% set _apache_max_clients = 5 %} 34 | {%- endif -%} 35 | {%- endif -%} 36 | 37 | # prefork MPM 38 | # StartServers: number of server processes to start 39 | # MinSpareServers: minimum number of server processes which are kept spare 40 | # MaxSpareServers: maximum number of server processes which are kept spare 41 | # MaxClients: maximum number of server processes allowed to start 42 | # MaxRequestsPerChild: maximum number of requests a server process serves 43 | 44 | StartServers {{ _apache_start_servers }} 45 | MinSpareServers {{ _apache_min_spare_servers }} 46 | MaxSpareServers {{ _apache_max_spare_servers }} 47 | MaxClients {{ _apache_max_clients }} 48 | ServerLimit {{ _apache_max_clients }} 49 | MaxRequestsPerChild {{ _apache_max_requests_per_child }} 50 | 51 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache/files/logrotate-apache: -------------------------------------------------------------------------------- 1 | /var/log/apache2/*.log { 2 | size 100M 3 | missingok 4 | rotate 10 5 | compress 6 | delaycompress 7 | notifempty 8 | create 640 root adm 9 | sharedscripts 10 | postrotate 11 | if /etc/init.d/apache2 status > /dev/null ; then \ 12 | /etc/init.d/apache2 reload > /dev/null; \ 13 | fi; 14 | endscript 15 | prerotate 16 | if [ -d /etc/logrotate.d/httpd-prerotate ]; then \ 17 | run-parts /etc/logrotate.d/httpd-prerotate; \ 18 | fi; \ 19 | endscript 20 | } 21 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart apache 3 | service: name=apache2 state=restarted 4 | become: true 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache/library/restore: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2014, Evan Kaufman 5 | # 6 | # This file is part of Ansible 7 | # 8 | # Ansible is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Ansible is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Ansible. If not, see . 20 | 21 | import glob 22 | import os 23 | 24 | DOCUMENTATION = """ 25 | --- 26 | module: restore 27 | author: Evan Kaufman 28 | short_description: Restores backup copy of a file 29 | description: 30 | - This module will restore a previously made backup of a given file 31 | options: 32 | path: 33 | required: true 34 | description: 35 | - The file to restore from backup. 36 | fail_on_missing: 37 | required: false 38 | default: "yes" 39 | choices: [ "yes", "no" ] 40 | description: 41 | - Causes failure if target file does not exist 42 | backup: 43 | required: false 44 | default: "no" 45 | choices: [ "yes", "no" ] 46 | description: 47 | - Create a backup file including the timestamp information so you can 48 | get the original file back if you somehow clobbered it incorrectly. 49 | which: 50 | required: false 51 | default: "oldest" 52 | choices: [ "oldest", "newest" ] 53 | description: 54 | - Which backup should be restored, if there are multiple available. 55 | """ 56 | 57 | EXAMPLES = """ 58 | # Restore oldest available backup of apache mod config 59 | - restore: path=/etc/apache2/mods-available/mpm_prefork.conf backup=no which=oldest 60 | 61 | # Restore newest available backup of service config, whilst backing up current file 62 | - restore: path=/etc/default/varnish backup=yes which=newest 63 | """ 64 | 65 | def main(): 66 | module = AnsibleModule( 67 | argument_spec=dict( 68 | path=dict(required=True), 69 | fail_on_missing=dict(default=True, type='bool'), 70 | backup=dict(default=False, type='bool'), 71 | which=dict(required=False, default='oldest', choices=[ 'oldest', 'newest'], type='str'), 72 | ), 73 | add_file_common_args=True, 74 | supports_check_mode=True 75 | ) 76 | 77 | path = os.path.expanduser(module.params['path']) 78 | fom = module.params['fail_on_missing'] 79 | backup = module.params['backup'] 80 | which = module.params['which'] 81 | 82 | if not os.path.exists(path): 83 | msg = "Path %s does not exist" % (path) 84 | if fom: 85 | module.fail_json(msg=msg) 86 | else: 87 | module.exit_json(changed=False, msg=msg) 88 | if not os.access(path, os.R_OK): 89 | module.fail_json(msg="Path %s not readable" % (path)) 90 | 91 | matched_backups = glob.glob(path + '.[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]@[0-9][0-9]:[0-9][0-9]~') 92 | 93 | if not matched_backups: 94 | module.exit_json(changed=False, msg="No backups found for %s" % (path)) 95 | 96 | if backup: 97 | backup_file = module.backup_local(path) 98 | 99 | matched_backups.sort() 100 | index = -1 if which == 'newest' else 0 101 | 102 | if not module.check_mode: 103 | try: 104 | module.atomic_move(matched_backups[index], path) 105 | except IOError: 106 | module.fail_json(msg="Failed to restore %s to %s" % (matched_backups[index], path)) 107 | 108 | module.exit_json(changed=True, msg="Restored file backup %s" % (matched_backups[index])) 109 | 110 | # this is magic, see lib/ansible/module_common.py 111 | #<> 112 | 113 | main() 114 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Apache packages 3 | apt: pkg={{ apache_packages }} state=present 4 | become: true 5 | 6 | - name: Enable Apache modules 7 | apache2_module: name={{ item }} state=present 8 | with_items: "{{ apache_modules }}" 9 | become: true 10 | notify: restart apache 11 | 12 | - name: Disable Apache modules 13 | apache2_module: name={{ item }} state=absent 14 | with_items: "{{ apache_disabled_modules }}" 15 | become: true 16 | notify: restart apache 17 | 18 | - name: Register Apache major/minor version 19 | shell: "apache2 -v | grep -i apache | perl -pe 's|^.*apache/([0-9]+[.][0-9]+)[.].*$|$1|i'" 20 | register: apache_version 21 | 22 | - debug: msg={{ apache_version.stdout }} 23 | 24 | - name: Configure apache log rotation 25 | copy: src=logrotate-apache dest=/etc/logrotate.d/apache2 owner=root group=root mode=644 26 | become: true 27 | 28 | - name: Configure Apache canonical server name (2.2) 29 | template: src=canonical dest=/etc/apache2/conf.d/canonical-servername.conf mode=0644 30 | notify: restart apache 31 | when: apache_version.stdout == '2.2' 32 | become: true 33 | 34 | - name: Configure Apache canonical server name (2.4) 35 | template: src=canonical dest=/etc/apache2/conf-available/canonical-servername.conf mode=0644 36 | when: apache_version.stdout == '2.4' 37 | become: true 38 | 39 | - name: Enable canonical config (2.4) 40 | command: a2enconf canonical-servername 41 | notify: restart apache 42 | when: apache_version.stdout == '2.4' 43 | become: true 44 | 45 | - name: Ensure Apache `NameVirtualHost` is 80 (2.2) 46 | lineinfile: regexp='^NameVirtualHost' line='NameVirtualHost *:80' dest=/etc/apache2/ports.conf backup=yes 47 | notify: restart apache 48 | when: apache_version.stdout == '2.2' 49 | become: true 50 | 51 | - name: Ensure Apache `Listen` is 80 52 | lineinfile: regexp='^Listen' line='Listen 80' dest=/etc/apache2/ports.conf backup=yes 53 | notify: restart apache 54 | become: true 55 | 56 | - name: Ensure existing Varnish installs won't cause port conflict on boot 57 | lineinfile: regexp='^START=' line='START=no' dest=/etc/default/varnish backup=no 58 | ignore_errors: true 59 | become: true 60 | 61 | - name: Ensure pristine mpm_prefork directives 62 | restore: path=/etc/apache2/mods-available/mpm_prefork.conf which=oldest backup=no fail_on_missing=no 63 | when: apache_version.stdout == '2.4' 64 | become: true 65 | 66 | - name: Change ownership of /var/www to deploy 67 | file: path=/var/www state=directory owner=deploy group=deploy 68 | become: true 69 | 70 | - shell: (cd /etc/apache2/sites-enabled/ && ls -1 000-default*) 71 | register: apache_default_vhost 72 | ignore_errors: true 73 | become: true 74 | 75 | - set_fact: 76 | wordpress__xmlrpc_allow: false 77 | when: wordpress__xmlrpc_allow is undefined 78 | 79 | - set_fact: 80 | wordpress__xmlrpc_whitelist: false 81 | when: wordpress__xmlrpc_whitelist is undefined 82 | 83 | - name: Disable default apache site 84 | command: a2dissite {{ apache_default_vhost.stdout }} removes=/etc/apache2/sites-enabled/{{ apache_default_vhost.stdout }} 85 | notify: restart apache 86 | when: apache_default_vhost.stdout != '' 87 | become: true 88 | 89 | - name: Create apache vhosts 90 | template: src=virtualhost dest=/etc/apache2/sites-available/{{ "%03d" | format(item.key_1) }}-{{ item.value }}.{{ domain }}.conf mode=0644 91 | with_nested_dict: "{{ apache_vhosts }}" # see ansible/ansible#9563 92 | when: item.key_0 == stage 93 | notify: restart apache 94 | become: true 95 | 96 | - name: Enable apache vhosts 97 | command: a2ensite {{ "%03d" | format(item.key_1) }}-{{ item.value }}.{{ domain }}.conf 98 | with_nested_dict: "{{ apache_vhosts }}" 99 | when: item.key_0 == stage 100 | notify: restart apache 101 | become: true 102 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache/templates/canonical: -------------------------------------------------------------------------------- 1 | UseCanonicalName On 2 | ServerName {{ domain }} 3 | -------------------------------------------------------------------------------- /lib/ansible/roles/apache/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | role_apache: true 3 | 4 | apache_packages: 5 | - apache2 6 | 7 | apache_modules: 8 | - expires 9 | - headers 10 | - rewrite 11 | - vhost_alias 12 | - deflate 13 | - setenvif 14 | - filter 15 | - mime 16 | - authz_core 17 | - authz_host 18 | - authz_user 19 | - authz_groupfile 20 | 21 | apache_disabled_modules: 22 | - autoindex 23 | 24 | apache_vhosts: 25 | local: 26 | 0: local 27 | staging: 28 | 1: staging 29 | 2: branch 30 | production: 31 | 3: production 32 | -------------------------------------------------------------------------------- /lib/ansible/roles/awstats/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: vagrant.yml 3 | when: has_vagrant.stdout 4 | -------------------------------------------------------------------------------- /lib/ansible/roles/awstats/tasks/vagrant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install awstats 3 | apt: pkg=awstats state=present 4 | become: true 5 | 6 | - name: Use logfile synced down from remote 7 | lineinfile: regexp='^LogFile=' line='LogFile="/tmp/apache2-remote-access.log"' dest=/etc/awstats/awstats.conf 8 | become: true 9 | 10 | - name: Use combined log format 11 | lineinfile: regexp='^LogFormat=' line='LogFormat=1' dest=/etc/awstats/awstats.conf 12 | become: true 13 | 14 | - name: Set primary site domain 15 | lineinfile: regexp='^SiteDomain=' line='SiteDomain="{{ domain }}"' dest=/etc/awstats/awstats.conf 16 | become: true 17 | 18 | - name: Set host aliases 19 | lineinfile: regexp='^HostAliases=' line='HostAliases="localhost 127.0.0.1 REGEX[^.*\.{{ domain | replace(".", "\\.") }}$]' dest=/etc/awstats/awstats.conf 20 | become: true 21 | 22 | - name: Enable Apache cgi module 23 | apache2_module: name=cgi state=present 24 | become: true 25 | notify: restart apache 26 | 27 | - name: Update vhosts for awstats web ui 28 | replace: dest=/etc/apache2/sites-available/{{ "%03d" | format(item.key_1) }}-{{ item.value }}.{{ domain }}.conf regexp='^([ \t]+)(LogLevel warn)' replace='\1\2\n\n\1Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch\n\1Alias /awstatsclasses "/usr/share/awstats/lib/"\n\1Alias /awstats-icon "/usr/share/awstats/icon/"\n\1Alias /awstatscss "/usr/share/doc/awstats/examples/css"\n\1ScriptAlias /awstats/ /usr/lib/cgi-bin/' 29 | with_nested_dict: "{{ apache_vhosts }}" # see ansible/ansible#9563 30 | when: item.key_0 == stage 31 | notify: restart apache 32 | become: true 33 | -------------------------------------------------------------------------------- /lib/ansible/roles/cleanup/files/varnish-health.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # expect a required positional argument 4 | STAGE_DOMAIN="$1" 5 | 6 | # display usage, if necessary 7 | if [ -z "$STAGE_DOMAIN" ]; then 8 | echo " 9 | Usage: $0 10 | 11 | Arguments: 12 | domain Stage and domain at which varnish should be checked, eg 'production.example.com' 13 | " 14 | exit 1 15 | fi 16 | 17 | # generate verbose output? (for debugging) 18 | VERBOSE= 19 | 20 | # minimum amount of seconds between service kicks 21 | KICK_THRESHOLD=300 22 | 23 | # current timestamp 24 | NOW=$(date +%s) 25 | 26 | STAMP_FILE="/tmp/varnish-check.${STAGE_DOMAIN}" 27 | 28 | function vprint { 29 | if [ -n "$VERBOSE" ]; then 30 | echo $@ 31 | fi 32 | } 33 | 34 | function health_check { 35 | local http_status=$(/usr/bin/curl -Is -m 1 --max-redirs 0 -w %{http_code} "http://${STAGE_DOMAIN}/varnish-status" -o /dev/null) 36 | vprint "Health check returned $http_status" 37 | [ "$http_status" != "200" ] 38 | } 39 | 40 | function health_kick { 41 | vprint "Kicking varnish" 42 | sudo service varnish restart > /dev/null 2>&1 43 | 44 | if health_check; then 45 | vprint "Kick unsuccessful, writing a timestamp" 46 | echo -n "$NOW" > "$STAMP_FILE" 47 | else 48 | rm -f "$STAMP_FILE" 49 | fi 50 | } 51 | 52 | function time_diff { 53 | if [ -e "$STAMP_FILE" ]; then 54 | local threshold=$1 55 | 56 | vprint "Previous timestamp exists..." 57 | 58 | LAST_RESTART=$(cat "$STAMP_FILE") 59 | TIME_DIFF=$(echo "$NOW - $LAST_RESTART" | bc) 60 | 61 | vprint "$TIME_DIFF seconds since last unsuccessful health kick" 62 | 63 | if [ "$TIME_DIFF" -ge "$threshold" ]; then 64 | vprint "Past threshold of $threshold" 65 | return 0 66 | else 67 | return 1 68 | fi 69 | else 70 | return 0 71 | fi 72 | } 73 | 74 | # if health check fails... 75 | if health_check; then 76 | # kick service if longer than $threshold since last restart... 77 | if time_diff $KICK_THRESHOLD; then 78 | health_kick 79 | fi 80 | fi 81 | -------------------------------------------------------------------------------- /lib/ansible/roles/cleanup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - set_fact: 3 | custom__packages: [] 4 | when: custom__packages is undefined 5 | 6 | - name: Install custom packages 7 | apt: pkg={{ item }} state=present 8 | with_items: "{{ custom__packages }}" 9 | become: true 10 | 11 | - name: Ensure logrotate runs hourly 12 | command: mv /etc/cron.daily/logrotate /etc/cron.hourly/logrotate 13 | args: 14 | creates: /etc/cron.hourly/logrotate 15 | removes: /etc/cron.daily/logrotate 16 | become: true 17 | 18 | - name: Set up wpcron via user-level crontab 19 | cron: 20 | args: 21 | name: "wpcron for {{stage}}" 22 | user: deploy 23 | job: "cd {{item.value}} && /usr/local/bin/wp core is-installed --path=$PWD --url='http://{{stage}}.{{domain}}/' && /usr/local/bin/wp cron event run --all --quiet --path=$PWD --url='http://{{stage}}.{{domain}}/'" 24 | with_dict: "{{ wp_cron_path }}" 25 | when: item.key == stage 26 | become: true 27 | 28 | - name: Configure varnish health-checking binary 29 | copy: src=varnish-health.sh dest=/usr/local/bin/varnish-health.sh owner=deploy group=deploy mode=0755 30 | when: role_varnish is defined 31 | become: true 32 | 33 | - name: Disable varnish health checking (as necessary) via user-level crontab 34 | cron: 35 | args: 36 | name: "varnish health check for {{stage}}" 37 | user: deploy 38 | state: absent 39 | when: role_varnish is not defined 40 | become: true 41 | 42 | - name: Set up varnish health checking via user-level crontab 43 | cron: 44 | args: 45 | name: "varnish health check for {{stage}}" 46 | user: deploy 47 | job: "/usr/local/bin/varnish-health.sh {{stage}}.{{domain}}" 48 | when: role_varnish is defined 49 | become: true 50 | 51 | - name: Configure wordpress autoupdate binary 52 | copy: src=update.sh dest=/usr/local/bin/wp-update owner=deploy group=deploy mode=0755 53 | become: true 54 | 55 | - set_fact: 56 | wordpress__autoupdate: false 57 | when: wordpress__autoupdate is undefined 58 | 59 | - set_fact: 60 | wordpress__autoupdate_skip_plugins: false 61 | when: wordpress__autoupdate_skip_plugins is undefined 62 | 63 | - set_fact: 64 | wordpress__autoupdate_skip_themes: false 65 | when: wordpress__autoupdate_skip_themes is undefined 66 | 67 | - name: Disable wordpress core and cli updates (as necessary) via user-level crontab 68 | cron: 69 | args: 70 | name: "daily wp auto updates for {{stage}}" 71 | user: deploy 72 | state: absent 73 | with_dict: "{{ wp_cron_path }}" 74 | when: wordpress__autoupdate == false and item.key == stage 75 | become: true 76 | 77 | - name: Set up wordpress core and cli updates via user-level crontab 78 | cron: 79 | args: 80 | name: "daily wp auto updates for {{stage}}" 81 | user: deploy 82 | job: "/usr/local/bin/wp-update {{ '--major' if wordpress__autoupdate == 'major' else '' }} {{ '--skip-plugins' if wordpress__autoupdate_skip_plugins else '' }} {{ '--skip-themes' if wordpress__autoupdate_skip_themes else '' }} {{ item.value }} http://{{stage}}.{{domain}}/" 83 | hour: 0 84 | minute: "{{ 0 if item.key == 'staging' else 10 if item.key == 'production' else 20 }}" 85 | with_dict: "{{ wp_cron_path }}" 86 | when: wordpress__autoupdate in ['major','minor'] and item.key == stage 87 | become: true 88 | 89 | - name: Generate init.d for installed evolution services 90 | template: src=evolution-init-d dest=/etc/init.d/evolution-wordpress mode=0755 91 | become: true 92 | -------------------------------------------------------------------------------- /lib/ansible/roles/cleanup/templates/evolution-init-d: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Source function library 4 | . /lib/lsb/init-functions 5 | 6 | start_services() { 7 | {% if role_mysql is defined %} 8 | service mysql start 9 | {% endif %} 10 | {% if role_apache is defined %} 11 | service apache2 start 12 | {% endif %} 13 | {% if role_varnish is defined %} 14 | service varnish start 15 | {% endif %} 16 | {% if role_pound is defined %} 17 | service pound start 18 | {% endif %} 19 | {% if role_firewall is defined %} 20 | service iptables-persistent start 21 | {% endif %} 22 | } 23 | 24 | stop_services() { 25 | {% if role_mysql is defined %} 26 | service mysql stop 27 | {% endif %} 28 | {% if role_apache is defined %} 29 | service apache2 stop 30 | {% endif %} 31 | {% if role_varnish is defined %} 32 | service varnish stop 33 | {% endif %} 34 | {% if role_pound is defined %} 35 | service pound stop 36 | {% endif %} 37 | {% if role_firewall is defined %} 38 | service iptables-persistent stop 39 | {% endif %} 40 | } 41 | 42 | restart_services() { 43 | {% if role_mysql is defined %} 44 | service mysql restart 45 | {% endif %} 46 | {% if role_apache is defined %} 47 | service apache2 graceful 48 | {% endif %} 49 | {% if role_varnish is defined %} 50 | service varnish restart 51 | {% endif %} 52 | {% if role_pound is defined %} 53 | service pound restart 54 | {% endif %} 55 | {% if role_firewall is defined %} 56 | service iptables-persistent restart 57 | {% endif %} 58 | } 59 | 60 | status_services() { 61 | {% if role_mysql is defined %} 62 | service mysql status 63 | {% endif %} 64 | {% if role_apache is defined %} 65 | service apache2 status 66 | {% endif %} 67 | {% if role_varnish is defined %} 68 | service varnish status 69 | {% endif %} 70 | {% if role_pound is defined %} 71 | service pound status 72 | {% endif %} 73 | {% if role_firewall is defined %} 74 | service iptables-persistent status 75 | {% endif %} 76 | } 77 | 78 | case "$1" in 79 | start) 80 | start_services 81 | ;; 82 | stop) 83 | stop_services 84 | ;; 85 | restart) 86 | restart_services 87 | ;; 88 | status) 89 | status_services 90 | ;; 91 | reboot) 92 | reboot 93 | ;; 94 | version) 95 | log_success_msg "Evolution Wordpress" "{{ evolve_version }}" 96 | ;; 97 | *) 98 | log_success_msg "Usage $0 {start|stop|restart|status|reboot|version}" 99 | exit 1 100 | ;; 101 | esac 102 | 103 | exit 0 104 | -------------------------------------------------------------------------------- /lib/ansible/roles/cleanup/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | wp_cron_path: 3 | local: /vagrant/web/wp 4 | staging: "/var/www/{{domain}}/staging/*/current/web/wp" 5 | production: "/var/www/{{domain}}/production/master/current/web/wp" 6 | -------------------------------------------------------------------------------- /lib/ansible/roles/common/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | swap__path: /swapfile 3 | swap__swappiness: 10 4 | swap__vfs_cache_pressure: 50 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/common/files/logrotate: -------------------------------------------------------------------------------- 1 | /var/log/evolution/*.log { 2 | rotate 10 3 | size 1M 4 | missingok 5 | notifempty 6 | compress 7 | } 8 | -------------------------------------------------------------------------------- /lib/ansible/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Domain 3 | debug: var=domain 4 | 5 | - name: Stage 6 | debug: var=stage 7 | 8 | - name: New Hostname 9 | debug: var=new_hostname 10 | 11 | - name: Determine static or dynamic resolv.conf 12 | shell: cat /etc/resolv.conf | grep "DO NOT EDIT THIS FILE BY HAND" 13 | ignore_errors: yes 14 | register: dynamic_resolv 15 | 16 | - name: Update nameservers in resolv.conf 17 | replace: dest=/etc/resolv.conf regexp="(nameserver [\w\d.:]+\n?)+" replace="nameserver 8.8.8.8\nnameserver 8.8.4.4\n\1" 18 | become: true 19 | 20 | - name: Update nameservers in dhclient.conf 21 | lineinfile: dest=/etc/dhcp/dhclient.conf regexp="^#?prepend domain-name-servers " line="prepend domain-name-servers 8.8.8.8, 8.8.4.4;" 22 | become: true 23 | when: dynamic_resolv.stdout != "" 24 | 25 | # see evolution/wordpress#126 26 | - name: Fetch kernel purging script (on control machine) 27 | local_action: get_url dest=/tmp/purge-kernels.py url=https://raw.githubusercontent.com/EvanK/ubuntu-purge-kernels/master/purge-kernels.py force=yes 28 | 29 | - name: Copy purging script to remote 30 | copy: src=/tmp/purge-kernels.py dest=/tmp/purge-kernels.py force=yes mode=0755 31 | become: true 32 | 33 | - name: Purge kernels as necessary 34 | command: /tmp/purge-kernels.py 35 | register: purgekernels_result 36 | ignore_errors: yes 37 | become: true 38 | when: not lookup('env','CI') 39 | 40 | - name: Show purged kernels 41 | debug: var=purgekernels_result 42 | 43 | - name: Update apt cache 44 | apt: update_cache=yes cache_valid_time="{{ 60 * 60 * 24 }}" 45 | become: true 46 | 47 | - name: Autoremove unused packages 48 | shell: DEBIAN_FRONTEND=noninteractive apt-get -y autoremove 49 | become: true 50 | 51 | - name: Install common packages 52 | apt: pkg={{ common_packages }} state=present 53 | become: true 54 | 55 | - name: Update `hostname` 56 | hostname: name="{{ new_hostname }}" 57 | become: true 58 | when: old_hostname != new_hostname 59 | 60 | - name: Update /etc/hosts 61 | replace: dest=/etc/hosts backup=yes regexp={{ old_hostname | replace(".", "[.]") }} replace={{ new_hostname }} 62 | become: true 63 | when: old_hostname != new_hostname 64 | 65 | - name: Configure logrotate for evolution logs 66 | copy: src=logrotate dest=/etc/logrotate.d/evolution mode=0644 67 | become: true 68 | 69 | - name: Test for git protocol (git://) connectivity 70 | command: curl -v -m 10 http://github.com:9418/ 71 | register: git_protocol_test 72 | ignore_errors: true 73 | args: 74 | warn: false 75 | 76 | - name: Bypass git protocol if necessary 77 | command: git config --global url."https://".insteadOf git:// 78 | when: "'connect() timed out' in git_protocol_test.stderr" 79 | 80 | - include: swap.yml 81 | when: ansible_swaptotal_mb == 0 82 | -------------------------------------------------------------------------------- /lib/ansible/roles/common/tasks/swap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - shell: "df {{ swap__path | dirname }} | awk '!/^Filesystem/ {print $NF}'" 3 | register: swapfile_mountpoint 4 | 5 | - debug: var=swapfile_mountpoint 6 | 7 | - name: Calculate ideal swapfile size 8 | template: src=swapsize.j2 dest=/tmp/swapsize 9 | 10 | - name: Fetch swapfile size 11 | shell: "cat /tmp/swapsize | tr -d '[:space:]'" 12 | register: swapfile_size_mb 13 | 14 | - debug: var=swapfile_size_mb.stdout 15 | 16 | - fail: msg="Could not determine swapfile size with available diskspace" 17 | when: swapfile_size_mb.stdout == '' 18 | 19 | - name: Write swapfile 20 | command: fallocate -l {{swapfile_size_mb.stdout}}M {{swap__path}} 21 | become: true 22 | 23 | - name: Set permissions 24 | file: path={{swap__path}} mode=600 owner=root group=root 25 | become: true 26 | 27 | - name: Create swapfile 28 | command: mkswap {{swap__path}} 29 | become: true 30 | 31 | - name: Enable swapfile 32 | command: swapon {{swap__path}} 33 | become: true 34 | 35 | - name: Add swapfile to /etc/fstab 36 | lineinfile: dest=/etc/fstab state=present line="{{ swap__path }} none swap sw 0 0" 37 | become: true 38 | 39 | - name: Configure vm.swappiness 40 | lineinfile: dest=/etc/sysctl.conf line="vm.swappiness = {{ swap__swappiness }}" regexp="^vm.swappiness\s*?=" state=present 41 | become: true 42 | 43 | - name: Configure vm.vfs_cache_pressure 44 | lineinfile: dest=/etc/sysctl.conf line="vm.vfs_cache_pressure = {{ swap__vfs_cache_pressure }}" regexp="^vm.vfs_cache_pressure\s*?=" state=present 45 | become: true 46 | 47 | - name: Reload sysctl 48 | command: sysctl -p 49 | become: true 50 | -------------------------------------------------------------------------------- /lib/ansible/roles/common/templates/swapsize.j2: -------------------------------------------------------------------------------- 1 | {# determine available space on root partition #} 2 | {%- for mp in ansible_mounts -%} 3 | {%- if mp.mount == swapfile_mountpoint.stdout -%} 4 | {%- set _disk_avail_mb = mp.size_available / 1024 / 1024 -%} 5 | {# if enough space for 2x current memory, use 1x for swap #} 6 | {%- if _disk_avail_mb >= (ansible_memtotal_mb * 2) -%} 7 | {{ ansible_memtotal_mb | round | int }} 8 | {# if enough space for 1x current memory, use 1/2 for swap #} 9 | {%- elif _disk_avail_mb >= ansible_memtotal_mb -%} 10 | {{ (ansible_memtotal_mb / 2) | round | int }} 11 | {# if enough space for 1/2 current memory, use 1/4 for swap #} 12 | {%- elif _disk_avail_mb >= (ansible_memtotal_mb / 2) -%} 13 | {{ (ansible_memtotal_mb / 4) | round | int }} 14 | {# if enough space for 1/4 current memory, use 1/8 for swap #} 15 | {%- elif _disk_avail_mb >= (ansible_memtotal_mb / 4) -%} 16 | {{ (ansible_memtotal_mb / 8) | round | int }} 17 | {%- endif -%} 18 | {# otherwise leave blank, for playbook to handle #} 19 | {%- endif -%} 20 | {%- endfor -%} 21 | -------------------------------------------------------------------------------- /lib/ansible/roles/common/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | old_hostname: "{{ lookup('pipe', 'hostname') }}" 3 | new_hostname: "{{ stage }}.{{ domain }}" 4 | 5 | common_packages: 6 | - autoconf 7 | - build-essential 8 | - curl 9 | - git-core 10 | -------------------------------------------------------------------------------- /lib/ansible/roles/debug/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install debug packages 3 | apt: pkg={{ debug_packages }} state=present 4 | become: true 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/debug/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | debug_packages: 3 | - htop 4 | - iftop 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | iptables__ipv6: 0 3 | 4 | fail2ban__whitelist: 5 | - 127.0.0.1/8 6 | fail2ban__ban_time: 600 7 | fail2ban__notification_email: root@localhost 8 | fail2ban__notify_on_ban: 0 9 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/files/init.d/iptables-persistent: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: iptables-persistent 5 | # Required-Start: mountkernfs $local_fs 6 | # Required-Stop: $local_fs 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # X-Start-Before: $network 10 | # X-Stop-After: $network 11 | # Short-Description: Set up iptables rules 12 | ### END INIT INFO 13 | 14 | # Based on https://github.com/zertrin/iptables-persistent 15 | 16 | PATH="/sbin:/bin:/usr/sbin:/usr/bin" 17 | 18 | # Include config file for iptables-persistent 19 | . /etc/default/iptables-persistent.conf 20 | 21 | . /lib/lsb/init-functions 22 | 23 | rc=0 24 | 25 | case "$1" in 26 | start) 27 | log_action_begin_msg "Starting iptables" 28 | 29 | if [ -e /var/run/iptables ]; then 30 | log_warning_msg "iptables is already started" 31 | exit 1 32 | else 33 | touch /var/run/iptables 34 | fi 35 | 36 | # if fail2ban is already running, stop it the time needed to load the new rules 37 | if [ -x /etc/init.d/fail2ban ]; then 38 | /etc/init.d/fail2ban stop 39 | fi 40 | 41 | if [ $ENABLE_ROUTING -ne 0 ]; then 42 | # Enable Routing 43 | echo 1 > /proc/sys/net/ipv4/ip_forward 44 | log_action_cont_msg "v4 Routing" 45 | if [ $IPV6 -ne 0 ]; then 46 | echo 1 >/proc/sys/net/ipv6/conf/all/forwarding 47 | log_action_cont_msg "v6 Routing" 48 | fi 49 | fi 50 | 51 | if [ $MODULES ]; then 52 | # Load Modules 53 | modprobe -a $MODULES 54 | log_action_cont_msg "Modules $MODULES" 55 | fi 56 | 57 | # Load saved rules 58 | if [ -f /etc/iptables/rules.v4 ]; then 59 | iptables-restore /etc/iptables/rules.v4 96 | if [ $? -ne 0 ]; then 97 | rc=1 98 | fi 99 | log_action_cont_msg "IPv4" 100 | 101 | if [ $IPV6 -ne 0 ]; then 102 | # Backup old rules 103 | cp /etc/iptables/rules.v6 /etc/iptables/rules.v6.bak 104 | # Save new rules 105 | ip6tables-save >/etc/iptables/rules.v6 106 | if [ $? -ne 0 ]; then 107 | rc=1 108 | fi 109 | log_action_cont_msg "IPv6" 110 | fi 111 | fi 112 | 113 | # stop fail2ban before flushing iptables chains 114 | if [ -x /etc/init.d/fail2ban ]; then 115 | /etc/init.d/fail2ban stop 116 | fi 117 | 118 | # Restore Default Policies 119 | iptables -P INPUT ACCEPT 120 | iptables -P FORWARD ACCEPT 121 | iptables -P OUTPUT ACCEPT 122 | 123 | # Flush rules on default tables 124 | iptables -F 125 | iptables -t nat -F 126 | iptables -t mangle -F 127 | 128 | if [ $IPV6 -ne 0 ]; then 129 | # Restore Default Policies 130 | ip6tables -P INPUT ACCEPT 131 | ip6tables -P FORWARD ACCEPT 132 | ip6tables -P OUTPUT ACCEPT 133 | 134 | # Flush rules on default tables 135 | ip6tables -F 136 | ip6tables -t mangle -F 137 | fi 138 | 139 | if [ $MODULES ]; then 140 | # Unload previously loaded modules 141 | modprobe -r $MODULES 142 | fi 143 | 144 | # Disable Routing if enabled 145 | if [ $ENABLE_ROUTING -ne 0 ]; then 146 | # Disable Routing 147 | echo 0 > /proc/sys/net/ipv4/ip_forward 148 | if [ $IPV6 -ne 0 ]; then 149 | echo 0 >/proc/sys/net/ipv6/conf/all/forwarding 150 | fi 151 | fi 152 | 153 | # start of fail2ban 154 | if [ -x /etc/init.d/fail2ban ]; then 155 | /etc/init.d/fail2ban start 156 | fi 157 | 158 | log_action_end_msg $rc 159 | ;; 160 | 161 | restart|force-reload) 162 | $0 stop 163 | $0 start 164 | ;; 165 | 166 | status) 167 | echo "Filter Rules:" 168 | echo "--------------" 169 | iptables -L -v 170 | echo "" 171 | echo "NAT Rules:" 172 | echo "-------------" 173 | iptables -t nat -L -v 174 | echo "" 175 | echo "Mangle Rules:" 176 | echo "----------------" 177 | iptables -t mangle -L -v 178 | 179 | if [ $IPV6 -ne 0 ]; then 180 | echo "**********" 181 | echo "** IPV6 **" 182 | echo "**********" 183 | echo "Filter Rules:" 184 | echo "--------------" 185 | ip6tables -L -v 186 | echo "" 187 | echo "Mangle Rules:" 188 | echo "----------------" 189 | ip6tables -t mangle -L -v 190 | fi 191 | ;; 192 | 193 | *) 194 | echo "Usage: $0 {start|stop|force-stop|restart|force-reload|status}" >&2 195 | exit 1 196 | ;; 197 | esac 198 | 199 | exit 0 200 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/files/iptables/rules.v4: -------------------------------------------------------------------------------- 1 | *filter 2 | # Set default chain policies 3 | :INPUT ACCEPT [0:0] 4 | :FORWARD ACCEPT [0:0] 5 | :OUTPUT ACCEPT [0:0] 6 | 7 | # Allows all loopback (lo0) traffic and drop all traffic to 127/8 that doesn't use lo0 8 | -A INPUT -i lo -j ACCEPT 9 | -A INPUT ! -i lo -d 127.0.0.0/8 -j REJECT 10 | 11 | # Drop any tcp packet that does not start a connection with a syn flag. 12 | -A INPUT -p tcp ! --syn -m state --state NEW -j DROP 13 | 14 | # Accepts all established inbound connections 15 | -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 16 | 17 | # Drop any invalid packet that could not be identified. 18 | -A INPUT -m state --state INVALID -j DROP 19 | 20 | # Blacklist (examples) 21 | #-A INPUT -i eth+ -s 198.51.100.1 -p tcp --dport 22 -j DROP # Example IP from RFC 5737 22 | #-A INPUT -i eth+ -s 203.0.113.34 -j DROP # Example IP from RFC 5737 23 | 24 | # Allow incoming FTP 25 | #-A INPUT -i eth+ -p tcp --dport 21 -j ACCEPT 26 | 27 | # Allow incoming SSH 28 | -A INPUT -i eth+ -p tcp --dport 22 -j ACCEPT 29 | 30 | # Allow incoming HTTP 31 | -A INPUT -i eth+ -p tcp --dport 80 -j ACCEPT 32 | 33 | # Allow incoming HTTPS 34 | -A INPUT -i eth+ -p tcp --dport 443 -j ACCEPT 35 | 36 | # Allow ping from inside to outside 37 | -A OUTPUT -o eth+ -p icmp --icmp-type echo-request -j ACCEPT 38 | -A INPUT -i eth+ -p icmp --icmp-type echo-reply -m limit --limit 2/s -j ACCEPT 39 | 40 | # Allow ping from outside to inside 41 | -A INPUT -i eth+ -p icmp --icmp-type echo-request -m limit --limit 2/s -j ACCEPT 42 | -A OUTPUT -o eth+ -p icmp --icmp-type echo-reply -j ACCEPT 43 | 44 | # Allow NTP 45 | #-A INPUT -i eth+ -p udp --dport 123 -j ACCEPT 46 | #-A OUTPUT -o eth+ -p udp --sport 123 -j ACCEPT 47 | 48 | # Allow DNS 49 | #-A OUTPUT -o eth+ -p tcp --dport 53 -j ACCEPT 50 | #-A OUTPUT -o eth+ -p udp --dport 53 -j ACCEPT 51 | #-A INPUT -i eth+ -p tcp --sport 53 -j ACCEPT 52 | #-A INPUT -i eth+ -p udp --sport 53 -j ACCEPT 53 | 54 | # Log dropped packets 55 | -N LOGGING 56 | -A INPUT -j LOGGING 57 | -A LOGGING -p tcp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [denied TCP] " --log-level 7 58 | -A LOGGING -p udp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [denied UDP] " --log-level 7 59 | -A LOGGING -p icmp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [denied ICMP] " --log-level 7 60 | 61 | # Reject all other inbound - default deny unless explicitly allowed policy: 62 | -A INPUT -j REJECT 63 | 64 | # Allow all other outbound traffic 65 | -A OUTPUT -j ACCEPT 66 | 67 | # Reject forwarded traffic, explicitely (despite chain policy) 68 | -A FORWARD -j REJECT 69 | 70 | COMMIT 71 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/files/iptables/rules.v6: -------------------------------------------------------------------------------- 1 | *filter 2 | :INPUT ACCEPT [0:0] 3 | :FORWARD ACCEPT [0:0] 4 | :OUTPUT ACCEPT [0:0] 5 | 6 | # allow all loopback traffic 7 | -A INPUT -i lo -j ACCEPT 8 | 9 | # allow all ICMP traffic 10 | -A INPUT -p icmpv6 -j ACCEPT 11 | 12 | # Drop any tcp packet that does not start a connection with a syn flag. 13 | -A INPUT -p tcp ! --syn -m state --state NEW -j DROP 14 | # packets belonging to an established connection or related to one can pass 15 | -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT 16 | # packets that are out-of-sequence are silently dropped 17 | -A INPUT -m state --state INVALID -j DROP 18 | 19 | # Allow incoming FTP 20 | #-A INPUT -i eth+ -p tcp --dport 21 -j ACCEPT 21 | 22 | # Allow incoming SSH 23 | -A INPUT -i eth+ -p tcp -m tcp --dport 22 -j ACCEPT 24 | 25 | # Allow incoming HTTP 26 | -A INPUT -i eth+ -p tcp -m tcp --dport 80 -j ACCEPT 27 | 28 | # Allow incoming HTTPS 29 | -A INPUT -i eth+ -p tcp -m tcp --dport 443 -j ACCEPT 30 | 31 | # Allow DNS 32 | #-A OUTPUT -o eth+ -p tcp --dport 53 -j ACCEPT 33 | #-A OUTPUT -o eth+ -p udp --dport 53 -j ACCEPT 34 | #-A INPUT -i eth+ -p tcp --sport 53 -j ACCEPT 35 | #-A INPUT -i eth+ -p udp --sport 53 -j ACCEPT 36 | 37 | # Log dropped packets 38 | -N LOGGING 39 | -A INPUT -j LOGGING 40 | -A LOGGING -p tcp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [INPUT6 ][denied TCP] " --log-level 7 41 | -A LOGGING -p udp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [INPUT6 ][denied UDP] " --log-level 7 42 | -A LOGGING -p icmp -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "iptables: [INPUT6 ][denied ICMP] " --log-level 7 43 | -A INPUT -j REJECT 44 | 45 | # allow outgoing traffic, explicitly (despite chain policy) 46 | -A OUTPUT -j ACCEPT 47 | 48 | # disallow forwarded traffic, explicitly (despite chain policy) 49 | -A FORWARD -j REJECT 50 | 51 | COMMIT 52 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart firewall 3 | service: name=iptables-persistent state=restarted 4 | become: true 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Attempt to install vanilla iptables-persistent 3 | apt: pkg=iptables-persistent state=latest update_cache=yes 4 | register: iptables_attempted 5 | ignore_errors: yes 6 | become: true 7 | 8 | # See https://forum.linode.com/viewtopic.php?t=9070&p=58732 9 | - name: Postinst workaround for iptables-persistent 10 | replace: dest=/var/lib/dpkg/info/iptables-persistent.postinst regexp='^(\s*?modprobe -q ip6?table_filter\s*?)$' replace='\1 || true' 11 | when: iptables_attempted is failed 12 | become: true 13 | 14 | - name: Install firewall packages 15 | apt: pkg={{ firewall_packages }} state=present 16 | become: true 17 | 18 | - name: Install iptables init script + rulesets 19 | copy: src={{ item }} dest=/etc/{{ item }} mode=0644 backup=yes 20 | become: true 21 | with_items: 22 | - init.d/iptables-persistent 23 | - iptables/rules.v4 24 | - iptables/rules.v6 25 | 26 | - name: Create iptables + fail2ban configs 27 | template: src={{ item }} dest=/etc/{{ item }} mode=0644 28 | become: true 29 | with_items: 30 | - default/iptables-persistent.conf 31 | - fail2ban/jail.local 32 | 33 | - name: Ensure iptables init script is executable 34 | file: path=/etc/init.d/iptables-persistent mode=0755 35 | notify: restart firewall 36 | become: true 37 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/templates/default/iptables-persistent.conf: -------------------------------------------------------------------------------- 1 | # A basic config file for the /etc/init.d/iptable-persistent script 2 | # 3 | 4 | # Should new manually added rules from command line be saved on reboot? Assign to a value different that 0 if you want this enabled. 5 | SAVE_NEW_RULES=0 6 | 7 | # Modules to load: 8 | #modules="ip_conntrack_ftp" #example for ftp conntracking 9 | modules="" 10 | 11 | # Enable Routing? Assign to a value different that 0 if you want this enabled. 12 | ENABLE_ROUTING=0 13 | 14 | # Enable IPv6? Assign to a value different that 0 if you want this enabled. 15 | IPV6={{ '1' if iptables__ipv6 else '0' }} 16 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/templates/fail2ban/jail.local: -------------------------------------------------------------------------------- 1 | ## Overrides of distribution-provided fail2ban jail.conf 2 | 3 | # Global definition of options, can be overridden in each jail afterwards. 4 | [DEFAULT] 5 | 6 | # Can be space separated IP addresses, CIDR masks or DNS hosts 7 | ignoreip = {{ ['127.0.0.1/8'] | union(fail2ban__whitelist) | unique | join(' ') }} 8 | 9 | # Global default ban time (in seconds) 10 | bantime = {{ fail2ban__ban_time }} 11 | 12 | # Destination email for interpolations in jail.{conf,local} 13 | destemail = {{ fail2ban__notification_email }} 14 | 15 | # Default banning action 16 | banaction = iptables-multiport 17 | 18 | # Default action (e.g. action_ for ban only, action_mw for ban + email, action_mwl for ban + email w relevant log entries) 19 | action = %(action_{{ 'mwl' if fail2ban__notify_on_ban else '' }})s 20 | 21 | 22 | [ssh] 23 | 24 | enabled = true 25 | port = ssh 26 | filter = sshd 27 | logpath = /var/log/auth.log 28 | maxretry = 6 29 | 30 | [ssh-ddos] 31 | 32 | enabled = true 33 | port = ssh 34 | filter = sshd-ddos 35 | logpath = /var/log/auth.log 36 | maxretry = 6 37 | 38 | [apache] 39 | 40 | enabled = true 41 | port = http,https 42 | filter = apache-auth 43 | logpath = /var/log/apache*/*error.log 44 | maxretry = 6 45 | 46 | [apache-overflows] 47 | 48 | enabled = true 49 | port = http,https 50 | filter = apache-overflows 51 | logpath = /var/log/apache*/*error.log 52 | maxretry = 2 53 | -------------------------------------------------------------------------------- /lib/ansible/roles/firewall/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | role_firewall: true 3 | 4 | firewall_packages: 5 | - iptables-persistent 6 | - fail2ban 7 | -------------------------------------------------------------------------------- /lib/ansible/roles/mail/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install mail package 3 | apt: pkg=postfix state=present 4 | become: true 5 | 6 | - name: Generate /etc/aliases 7 | template: src=aliases dest=/etc/aliases mode=0644 8 | become: true 9 | 10 | - name: Update mail aliases 11 | command: newaliases 12 | become: true 13 | -------------------------------------------------------------------------------- /lib/ansible/roles/mail/templates/aliases: -------------------------------------------------------------------------------- 1 | # See man 5 aliases for format 2 | postmaster: deploy 3 | deploy: {{ mail__postmaster }} 4 | -------------------------------------------------------------------------------- /lib/ansible/roles/mysql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install MySQL packages 3 | apt: pkg={{ mysql_packages }} state=present 4 | become: true 5 | 6 | - name: Update my.conf's bind-address 7 | lineinfile: dest=/etc/mysql/my.cnf backup=yes regexp=^bind-address line='bind-address = 0.0.0.0' 8 | become: true 9 | 10 | - name: Restart mysql 11 | service: name=mysql state=restarted 12 | become: true 13 | 14 | - name: Create MySQL database 15 | mysql_db: name="{{ mysql.name }}_{{ stage }}" 16 | become: true 17 | 18 | - name: Create MySQL user 19 | mysql_user: name={{ mysql.user }} host={{ mysql.host }} password={{ mysql.password }} priv="{{ mysql.name }}_{{ stage }}.*:GRANT,ALL" append_privs=yes 20 | become: true 21 | -------------------------------------------------------------------------------- /lib/ansible/roles/mysql/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | role_mysql: true 3 | 4 | mysql_packages: 5 | - libapache2-mod-auth-mysql 6 | - mysql-server-5.6 7 | - mysql-server-core-5.6 8 | - python-mysqldb 9 | -------------------------------------------------------------------------------- /lib/ansible/roles/newrelic/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart newrelic-sysmond 3 | service: name=newrelic-sysmond state=restarted 4 | become: true 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/newrelic/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install repository key 3 | apt_key: url=https://download.newrelic.com/548C16BF.gpg state=present 4 | become: true 5 | 6 | - name: Configure repository source list 7 | apt_repository: repo='deb http://apt.newrelic.com/debian/ newrelic non-free' state=present 8 | become: true 9 | 10 | # if upgrading to 7 or downgrading to 5, remove & purge existing agent, let it install again with new php 11 | - name: Remove and purge existing agent (when upgrading/downgrading PHP) 12 | apt: name=newrelic-php5 state=absent purge=yes 13 | when: php_downgrade == true or php_upgrade == true 14 | become: true 15 | 16 | - name: Install PHP agent and sys monitor 17 | apt: name={{ nr_packages }} state=latest update_cache=yes 18 | become: true 19 | vars: 20 | nr_packages: 21 | - newrelic-php5 22 | - newrelic-sysmond 23 | 24 | - name: Configure PHP agent license key and app name 25 | lineinfile: dest={{ php_mods_path }}/newrelic.ini regexp="^{{ item.key }}" line="{{ item.key }} = '{{ item.value }}'" 26 | become: true 27 | with_dict: 28 | newrelic.appname: "{{ domain }} ({{ stage }})" 29 | newrelic.license: "{{ monitoring.newrelic }}" 30 | notify: restart apache 31 | 32 | - name: Configure and start sys monitor 33 | command: nrsysmond-config --set license_key={{ monitoring.newrelic }} 34 | become: true 35 | notify: restart newrelic-sysmond 36 | -------------------------------------------------------------------------------- /lib/ansible/roles/node/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add Node apt-repository 3 | apt_repository: repo='ppa:chris-lea/node.js' state=present update_cache=yes 4 | become: true 5 | 6 | - name: Install node 7 | apt: pkg=nodejs state=present 8 | become: true 9 | 10 | - name: Install global node modules 11 | command: npm install -g {{ item }} 12 | with_items: "{{ node_modules }}" 13 | become: true 14 | -------------------------------------------------------------------------------- /lib/ansible/roles/node/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | node_modules: 3 | - bower 4 | -------------------------------------------------------------------------------- /lib/ansible/roles/php-hardened/tasks/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fetch stable source 3 | get_url: url={{ suhosin.url }} dest=/tmp/src-suhosin.tar.gz validate_certs=False 4 | become: true 5 | 6 | - name: Hash source for checksum 7 | shell: sha1sum /tmp/src-suhosin.tar.gz 8 | register: suhosin_shahash 9 | become: true 10 | 11 | - debug: var=suhosin_shahash.stdout 12 | 13 | - fail: msg="Suhosin source failed SHA1 hash" 14 | when: suhosin_shahash.stdout.find(suhosin.sha) == -1 15 | 16 | - name: Expand source package 17 | unarchive: src=/tmp/src-suhosin.tar.gz dest=/tmp/ copy=no 18 | become: true 19 | 20 | - shell: ls -1d /tmp/suhosin* 21 | register: suhosin_srcdir 22 | become: true 23 | 24 | - debug: var=suhosin_srcdir.stdout 25 | 26 | - name: Compile and install extension 27 | command: "{{ item }} chdir={{ suhosin_srcdir.stdout }}" 28 | become: true 29 | with_items: 30 | - phpize 31 | - ./configure 32 | - make 33 | - make install 34 | 35 | - name: Clean up after ourselves 36 | shell: rm -rf /tmp/*suhosin* 37 | args: 38 | warn: false 39 | become: true 40 | -------------------------------------------------------------------------------- /lib/ansible/roles/php-hardened/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Disable allow_url_fopen 3 | lineinfile: dest={{ php_conf_path }}/apache2/php.ini backup=yes regexp='^[;# ]*allow_url_fopen' line='allow_url_fopen = Off' 4 | when: php__version_7 == false 5 | become: true 6 | 7 | - include: suhosin.yml 8 | when: php__version_7 == false 9 | 10 | # NOTE: Suhosin is pre-alpha for php7, so we'll just have to do without it for now 11 | # see: https://github.com/sektioneins/suhosin7 12 | -------------------------------------------------------------------------------- /lib/ansible/roles/php-hardened/tasks/suhosin.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install apg (for cryptkeys) 3 | apt: name=apg state=present 4 | become: true 5 | 6 | - name: Generate session cryptkey 7 | shell: apg -m 32 | sed 's/[^a-zA-Z0-9]//g' 8 | register: generated_session_cryptkey 9 | 10 | - debug: var=generated_session_cryptkey 11 | 12 | - name: Is suhosin already installed? 13 | stat: path={{ suhosin.ini }} 14 | register: suhosin_installed 15 | 16 | - name: Find existing session cryptkey 17 | shell: cat {{ suhosin.ini }} | sed -n -e 's/^suhosin\.session\.cryptkey = //p' 18 | register: existing_session_cryptkey 19 | when: suhosin_installed.stat.exists == True 20 | 21 | - debug: var=existing_session_cryptkey 22 | when: suhosin_installed.stat.exists == True 23 | 24 | - include: build.yml 25 | when: suhosin_installed.stat.exists == False 26 | 27 | - name: Generate ini config for extension 28 | template: src=suhosin.ini.j2 dest={{ suhosin.ini }} 29 | become: true 30 | 31 | - name: Enable extension 32 | command: php5enmod suhosin 33 | become: true 34 | notify: restart apache 35 | -------------------------------------------------------------------------------- /lib/ansible/roles/php-hardened/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | php_disable_functions: 3 | - exec 4 | - shell_exec 5 | - system 6 | - passthru 7 | 8 | suhosin: 9 | ini: "{{ php_mods_path }}/suhosin.ini" 10 | url: https://download.suhosin.org/suhosin-0.9.38.tar.gz 11 | sha: 20af6379c0ff9879c5ed69452a6c38b7b3e76748 12 | -------------------------------------------------------------------------------- /lib/ansible/roles/php/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # default to php5 when this var is not set 3 | - set_fact: 4 | php__version_7: false 5 | when: php__version_7 is undefined 6 | 7 | - debug: var=php__version_7 8 | 9 | - name: Register installed PHP major version 10 | shell: "php --version | grep -i cli | perl -pe 's|^PHP ([0-9]+)[.].*$|$1|i'" 11 | ignore_errors: true 12 | register: php_major_version 13 | 14 | - debug: msg={{ php_major_version }} 15 | 16 | - set_fact: 17 | php_upgrade: '{{ php_major_version.stdout == "5" and php__version_7 == true }}' 18 | 19 | - set_fact: 20 | php_downgrade: '{{ php_major_version.stdout == "7" and php__version_7 == false }}' 21 | 22 | - debug: msg='{{ "Upgrading" if php_upgrade else "Downgrading" if php_downgrade else "No change in" }} PHP major version' 23 | 24 | # remove php7 as necessary before we install php5 25 | - include: php7-uninstall.yml 26 | when: php_downgrade 27 | become: true 28 | 29 | # remove php5 as necessary before we install php7 30 | - include: php5-uninstall.yml 31 | when: php_upgrade 32 | become: true 33 | 34 | - include: php5-install.yml 35 | when: php__version_7 == false 36 | become: true 37 | 38 | - include: php7-install.yml 39 | when: php__version_7 == true 40 | become: true 41 | 42 | - name: Register php_conf_path for subsequent roles 43 | set_fact: 44 | php_conf_path: "{{ php7_conf_path if php__version_7 == true else php5_conf_path }}" 45 | 46 | - debug: var=php_conf_path 47 | 48 | - name: Register php_mods_path for subsequent roles 49 | set_fact: 50 | php_mods_path: "{{ php7_mods_path if php__version_7 == true else php5_mods_path }}" 51 | 52 | - debug: var=php_mods_path 53 | -------------------------------------------------------------------------------- /lib/ansible/roles/php/tasks/php5-install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install PHP packages 3 | apt: pkg={{ php5_packages }} 4 | become: true 5 | 6 | - name: Install PECL packages 7 | command: pecl install {{ item if item is string else item.name + '-' + item.version }} creates="{{ php5_lib_path }}/*/{{ item if item is string else item.name }}.so" 8 | with_items: "{{ pecl5_packages }}" 9 | become: true 10 | 11 | - name: Enable PECL packages 12 | lineinfile: line="extension={{ item if item is string else item.name }}.so" 13 | dest="{{ php5_mods_path }}/{{ item if item is string else item.name }}.ini" 14 | create=yes 15 | with_items: "{{ pecl5_packages }}" 16 | become: true 17 | 18 | - name: Calculate PHP memory_limit 19 | command: echo "{{ 256 if ansible_memtotal_mb > 2048 else 128 }}" 20 | register: calc_php_memory_limit 21 | ignore_errors: true 22 | when: php__memory_limit is undefined 23 | 24 | - name: Update php.ini's date.timezone 25 | lineinfile: dest={{ php5_conf_path }}/apache2/php.ini backup=yes regexp='^[;# ]*date\.timezone' line='date.timezone = "America/Chicago"' 26 | become: true 27 | 28 | - name: Update php.ini's memory_limit 29 | lineinfile: dest={{ php5_conf_path }}/apache2/php.ini backup=yes regexp='^[;# ]*memory_limit' line='memory_limit = {{ calc_php_memory_limit.stdout if calc_php_memory_limit.stdout is defined else php__memory_limit }}M' 30 | become: true 31 | 32 | - name: Update php.ini's upload_max_filesize 33 | lineinfile: dest={{ php5_conf_path }}/apache2/php.ini backup=yes regexp='^[;# ]*upload_max_filesize' line='upload_max_filesize = 16M' 34 | become: true 35 | -------------------------------------------------------------------------------- /lib/ansible/roles/php/tasks/php5-uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Uninstall PECL packages 3 | command: pecl uninstall {{ item if item is string else item.name + '-' + item.version }} removes={{ php5_mods_path }}/{{ item if item is string else item.name }}.ini 4 | with_items: "{{ pecl5_packages }}" 5 | become: true 6 | 7 | - name: Remove PECL ini files as necessary 8 | file: state=absent path="{{ php5_mods_path }}/{{ item if item is string else item.name }}.ini" 9 | with_items: "{{ pecl5_packages }}" 10 | become: true 11 | 12 | - name: Uninstall PHP packages 13 | apt: state=absent pkg={{ php5_packages }} 14 | become: true 15 | -------------------------------------------------------------------------------- /lib/ansible/roles/php/tasks/php7-install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install PHP7 ppa 3 | apt_repository: repo="ppa:ondrej/php" update_cache=yes 4 | become: true 5 | 6 | - name: Install PHP packages 7 | apt: pkg={{ php7_packages }} 8 | become: true 9 | 10 | - name: Install PECL packages 11 | command: pecl install {{ item if item is string else item.name + '-' + item.version }} creates="{{ php7_lib_path }}/*/{{ item if item is string else item.name }}.so" 12 | with_items: "{{ pecl7_packages }}" 13 | become: true 14 | 15 | - name: Enable PECL packages 16 | lineinfile: line="extension={{ item if item is string else item.name }}.so" 17 | dest="{{ php7_mods_path }}/{{ item if item is string else item.name }}.ini" 18 | create=yes 19 | with_items: "{{ pecl7_packages }}" 20 | become: true 21 | 22 | - name: Calculate PHP memory_limit 23 | command: echo "{{ 256 if ansible_memtotal_mb > 2048 else 128 }}" 24 | register: calc_php_memory_limit 25 | ignore_errors: true 26 | when: php__memory_limit is undefined 27 | 28 | - name: Update php.ini's date.timezone 29 | lineinfile: dest={{ php7_conf_path }}/apache2/php.ini backup=yes regexp='^[;# ]*date\.timezone' line='date.timezone = "America/Chicago"' 30 | become: true 31 | 32 | - name: Update php.ini's memory_limit 33 | lineinfile: dest={{ php7_conf_path }}/apache2/php.ini backup=yes regexp='^[;# ]*memory_limit' line='memory_limit = {{ calc_php_memory_limit.stdout if calc_php_memory_limit.stdout is defined else php__memory_limit }}M' 34 | become: true 35 | 36 | - name: Update php.ini's upload_max_filesize 37 | lineinfile: dest={{ php7_conf_path }}/apache2/php.ini backup=yes regexp='^[;# ]*upload_max_filesize' line='upload_max_filesize = 16M' 38 | become: true 39 | -------------------------------------------------------------------------------- /lib/ansible/roles/php/tasks/php7-uninstall.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Uninstall PECL packages 3 | command: pecl uninstall {{ item if item is string else item.name + '-' + item.version }} removes={{ php7_mods_path }}/{{ item if item is string else item.name }}.ini 4 | with_items: "{{ pecl7_packages }}" 5 | become: true 6 | 7 | - name: Remove PECL ini files as necessary 8 | file: state=absent path="{{ php7_mods_path }}/{{ item if item is string else item.name }}.ini" 9 | with_items: "{{ pecl7_packages }}" 10 | become: true 11 | 12 | - name: Uninstall PHP packages 13 | apt: state=absent pkg={{ php7_packages }} 14 | become: true 15 | 16 | - name: Remove PHP7 ppa 17 | apt_repository: repo="ppa:ondrej/php" state=absent update_cache=yes 18 | become: true 19 | -------------------------------------------------------------------------------- /lib/ansible/roles/php/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pecl5_packages: 3 | - { name: 'scream', version: '0.1.0'} 4 | 5 | pecl7_packages: [] 6 | 7 | php5_packages: 8 | - libapache2-mod-php5 9 | - php-pear 10 | - php5 11 | - php5-cli 12 | - php5-common 13 | - php5-curl 14 | - php5-gd 15 | - php5-mcrypt 16 | - php5-mysql 17 | - php5-dev 18 | 19 | php7_packages: 20 | - libapache2-mod-php7.1 21 | - php-pear 22 | - php7.1 23 | - php7.1-cli 24 | - php7.1-common 25 | - php7.1-curl 26 | - php7.1-gd 27 | - php7.1-mcrypt 28 | - php7.1-mysql 29 | - php7.1-mbstring 30 | - php7.1-bcmath 31 | - php7.1-bz2 32 | - php7.1-dba 33 | - php7.1-soap 34 | - php7.1-zip 35 | - php7.1-xml 36 | - php7.1-dev 37 | 38 | php5_conf_path: '/etc/php5' 39 | php7_conf_path: '/etc/php/7.1' 40 | 41 | php5_lib_path: '/usr/lib/php5' 42 | php7_lib_path: '/usr/lib/php' 43 | 44 | php5_mods_path: '{{ php5_conf_path }}/mods-available' 45 | php7_mods_path: '{{ php7_conf_path }}/mods-available' 46 | -------------------------------------------------------------------------------- /lib/ansible/roles/pound/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart pound 3 | service: name=pound state=restarted 4 | become: true 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/pound/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://launchpad.net/~unleashedtech/+archive/ubuntu/pound-2.7 3 | - name: Register pound 2.7 ppa 4 | apt_repository: repo='ppa:unleashedtech/pound-2.7' 5 | become: true 6 | 7 | - name: Install pound package 8 | apt: pkg=pound state=latest update_cache=yes 9 | become: true 10 | 11 | - name: Copy pound configuration file 12 | template: src=pound.cfg dest=/etc/pound/pound.cfg mode=0644 13 | notify: restart pound 14 | become: true 15 | 16 | - name: Copy SSL certificates 17 | copy: src=./files/ssl/{{ item }}.{{ domain }}.pem dest=/etc/pound/{{ item }}.{{ domain }}.pem mode=0644 18 | with_items: 19 | - local 20 | - staging 21 | - production 22 | notify: restart pound 23 | become: true 24 | 25 | - name: Enable pound 26 | lineinfile: regexp='^startup=0' line='startup=1' dest=/etc/default/pound backup=yes 27 | notify: restart pound 28 | become: true 29 | 30 | - name: Configure HTTPS Forwarded Proto detection in Apache (2.2) 31 | copy: content="SetEnvIf X-Forwarded-Proto ^https$ HTTPS=on\n" dest=/etc/apache2/conf.d/https-forwarded-proto.conf mode=0644 32 | notify: restart apache 33 | when: apache_version.stdout == '2.2' 34 | become: true 35 | 36 | - name: Configure HTTPS Forwarded Proto detection in Apache (2.4) 37 | copy: content="SetEnvIf X-Forwarded-Proto ^https$ HTTPS=on\n" dest=/etc/apache2/conf-available/https-forwarded-proto.conf mode=0644 38 | when: apache_version.stdout == '2.4' 39 | become: true 40 | 41 | - name: Enable canonical config (2.4) 42 | command: a2enconf https-forwarded-proto 43 | notify: restart apache 44 | when: apache_version.stdout == '2.4' 45 | become: true 46 | -------------------------------------------------------------------------------- /lib/ansible/roles/pound/templates/pound.cfg: -------------------------------------------------------------------------------- 1 | ## Minimal sample pound.cfg 2 | ## 3 | ## see pound(8) for details 4 | 5 | 6 | ###################################################################### 7 | ## global options: 8 | 9 | User "www-data" 10 | Group "www-data" 11 | #RootJail "/chroot/pound" 12 | 13 | ## Logging: (goes to syslog by default) 14 | ## 0 no logging 15 | ## 1 normal 16 | ## 2 extended 17 | ## 3 Apache-style (common log format) 18 | LogLevel 1 19 | 20 | ## check backend every X secs: 21 | Alive 60 22 | 23 | ## use hardware-accelleration card supported by openssl(1): 24 | #SSLEngine "" 25 | 26 | # poundctl control socket 27 | Control "/var/run/pound/poundctl.socket" 28 | 29 | 30 | ###################################################################### 31 | ## listen, redirect and ... to: 32 | 33 | ## redirect all requests on port 443 ("ListenHTTPS") to the local webserver (see "Service" below): 34 | ListenHTTPS 35 | Address 0.0.0.0 36 | Port 443 37 | 38 | HeadRemove "X-Forwarded-Proto" 39 | AddHeader "X-Forwarded-Proto: https" 40 | 41 | {% for cert in ['production','staging','local'] %} 42 | Cert "/etc/pound/{{ cert }}.{{ domain }}.pem" 43 | 44 | Service 45 | {% if cert == 'production' %} 46 | HeadRequire "Host: .*((www|{{ cert }})\.)?{{ domain|replace('.','\\.') }}.*" 47 | {% else %} 48 | HeadRequire "Host: .*{{ cert }}\.{{ domain|replace('.','\\.') }}.*" 49 | {% endif %} 50 | BackEnd 51 | Address 127.0.0.1 52 | Port 80 53 | End 54 | End 55 | {% endfor %} 56 | 57 | ## https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers 58 | Ciphers "ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS" 59 | SSLHonorCipherOrder 1 60 | Disable SSLv2 61 | Disable SSLv3 62 | 63 | End 64 | -------------------------------------------------------------------------------- /lib/ansible/roles/pound/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | role_pound: true 3 | -------------------------------------------------------------------------------- /lib/ansible/roles/prepare/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test distro and version, as we only support specifically Ubuntu 14 3 | fail: msg="{{ansible_distribution}} {{ansible_distribution_version}} is not supported" 4 | when: ansible_distribution != "Ubuntu" or ansible_distribution_major_version != "14" 5 | 6 | - name: Test existance of evolution init script 7 | stat: path=/etc/init.d/evolution-wordpress 8 | register: evo_init 9 | 10 | - name: Stop any existing evolution services 11 | service: name=evolution-wordpress state=stopped 12 | become: true 13 | when: evo_init.stat.exists == True 14 | -------------------------------------------------------------------------------- /lib/ansible/roles/snapshots/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure backup parameters 3 | set_fact: 4 | snapshots_config: 5 | stage: "{{ stage }}" 6 | domain: "{{ domain }}" 7 | dbname: "{{ mysql.name}}" 8 | releasepath: "/var/www/{{domain}}/production/master" 9 | method: "{{ snapshots__method | default() }}" 10 | credentials: "{{ snapshots__credentials | default() }}" 11 | interval: "{{ snapshots__interval | default() }}" 12 | retention: "{{ snapshots__retention | default() }}" 13 | retention_lag: "{{ snapshots__retention_lag | default() }}" 14 | when: > 15 | snapshots__method is defined 16 | or snapshots__credentials is defined 17 | or snapshots__interval is defined 18 | or snapshots__retention is defined 19 | or snapshots__retention_lag is defined 20 | 21 | - name: Install apt packages 22 | apt: pkg={{ snapshots_packages }} state=present 23 | when: snapshots_config is defined 24 | become: true 25 | 26 | - name: Install pip packages 27 | pip: name={{ snapshots_pip_packages }} state=latest 28 | when: snapshots_config is defined 29 | become: true 30 | 31 | - name: Install backup script 32 | copy: src=backup.py dest=/usr/local/bin/snapshot-backup.py owner=deploy group=deploy mode=0755 33 | when: snapshots_config is defined 34 | become: true 35 | 36 | - name: Disable production backups 37 | cron: 38 | args: 39 | name: "evolution interval backups for {{stage}}" 40 | user: deploy 41 | state: absent 42 | when: snapshots_config is undefined and stage == "production" 43 | become: true 44 | 45 | - name: Set up production backups via user-level crontab 46 | cron: 47 | args: 48 | name: "evolution interval backups for {{stage}}" 49 | user: deploy 50 | job: "/usr/local/bin/snapshot-backup.py --quiet -c '{{ snapshots_config | to_json }}'" 51 | when: stage == "production" and snapshots_config is defined 52 | become: true 53 | -------------------------------------------------------------------------------- /lib/ansible/roles/snapshots/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | snapshots_packages: 3 | - python-libcloud 4 | - python-pip 5 | 6 | snapshots_pip_packages: 7 | - grandfatherson 8 | - tabulate 9 | - certifi 10 | -------------------------------------------------------------------------------- /lib/ansible/roles/user/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check for vagrant user 3 | command: id -u vagrant 4 | register: has_vagrant 5 | ignore_errors: true 6 | 7 | - name: Add vagrant user to www-data 8 | user: name=vagrant append=yes groups=www-data 9 | when: has_vagrant.stdout 10 | become: true 11 | 12 | - name: Create deploy group 13 | group: name=deploy state=present system=no 14 | become: true 15 | 16 | - name: Create deploy user 17 | user: name=deploy append=yes group=deploy groups=www-data shell=/bin/bash comment="Created by Evolution WordPress" 18 | become: true 19 | 20 | - name: Grant sudo access to deploy user 21 | copy: content="%deploy ALL=(ALL) NOPASSWD:ALL" dest=/etc/sudoers.d/deploy mode=0440 force=no 22 | become: true 23 | 24 | - name: Add www-data user to deploy 25 | user: name=www-data append=yes groups=deploy 26 | become: true 27 | 28 | - name: Create /home/deploy/.ssh 29 | file: path=/home/deploy/.ssh state=directory mode=0755 owner=deploy group=deploy 30 | become: true 31 | 32 | - name: Copy deploy private key 33 | copy: src=./files/ssh/id_rsa dest=/home/deploy/.ssh/id_rsa owner=deploy group=deploy mode=0600 34 | become: true 35 | 36 | - name: Copy deploy public key 37 | copy: src=./files/ssh/id_rsa.pub dest=/home/deploy/.ssh/id_rsa.pub owner=deploy group=deploy mode=0600 38 | become: true 39 | 40 | - name: Set deploy key as authorized key 41 | copy: src=./files/ssh/id_rsa.pub dest=/home/deploy/.ssh/authorized_keys owner=deploy group=deploy mode=0600 42 | become: true 43 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/files/etc-default-varnish: -------------------------------------------------------------------------------- 1 | # Should we start varnishd at boot? Set to "no" to disable. 2 | START=yes 3 | 4 | # Maximum number of open files (for ulimit -n) 5 | NFILES=131072 6 | 7 | # Maximum locked memory size (for ulimit -l) 8 | MEMLOCK=82000 9 | 10 | # Main configuration file. You probably want to change it :) 11 | VARNISH_VCL_CONF=/etc/varnish/production.vcl 12 | 13 | # Default address and port to bind to 14 | VARNISH_LISTEN_ADDRESS= 15 | VARNISH_LISTEN_PORT=80 16 | 17 | # Telnet admin interface listen address and port 18 | VARNISH_ADMIN_LISTEN_ADDRESS=127.0.0.1 19 | VARNISH_ADMIN_LISTEN_PORT=6082 20 | 21 | # The minimum number of worker threads to start 22 | VARNISH_MIN_THREADS=100 23 | 24 | # The Maximum number of worker threads to start 25 | VARNISH_MAX_THREADS=5000 26 | 27 | # Idle timeout for worker threads 28 | VARNISH_THREAD_TIMEOUT=120 29 | 30 | # Cache file location 31 | VARNISH_STORAGE_FILE=/var/lib/varnish/$INSTANCE/varnish_storage.bin 32 | 33 | # Cache file size: in bytes, optionally using k / M / G / T suffix, 34 | # or in percentage of available disk space using the % suffix. 35 | VARNISH_STORAGE_SIZE=512M 36 | 37 | # File containing administration secret 38 | VARNISH_SECRET_FILE=/etc/varnish/secret 39 | 40 | # Backend storage specification 41 | VARNISH_STORAGE="file,${VARNISH_STORAGE_FILE},${VARNISH_STORAGE_SIZE}" 42 | 43 | # Default TTL used when the backend does not specify one 44 | VARNISH_TTL=120 45 | 46 | # DAEMON_OPTS is used by the init script. If you add or remove options, make 47 | # sure you update this section, too. 48 | DAEMON_OPTS="-a ${VARNISH_LISTEN_ADDRESS}:${VARNISH_LISTEN_PORT} \ 49 | -f ${VARNISH_VCL_CONF} \ 50 | -T ${VARNISH_ADMIN_LISTEN_ADDRESS}:${VARNISH_ADMIN_LISTEN_PORT} \ 51 | -t ${VARNISH_TTL} \ 52 | -w ${VARNISH_MIN_THREADS},${VARNISH_MAX_THREADS},${VARNISH_THREAD_TIMEOUT} \ 53 | -S ${VARNISH_SECRET_FILE} \ 54 | -s ${VARNISH_STORAGE}" 55 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/files/etc-varnish/conf.d/fetch/wordpress.vcl: -------------------------------------------------------------------------------- 1 | # Drop any cookies Wordpress tries to send back to the client. 2 | if (!(req.url ~ "wp-(login|admin)")) { 3 | unset beresp.http.set-cookie; 4 | } 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/files/etc-varnish/conf.d/receive/wordpress.vcl: -------------------------------------------------------------------------------- 1 | # Pass all login requests straight through 2 | if (req.url ~ "wp-login") { 3 | return (pass); 4 | } 5 | # Pipe all admin requests directly 6 | if (req.url ~ "wp-admin") { 7 | return (pipe); 8 | } 9 | 10 | # Pass all requests containing a wp- or wordpress_ cookie 11 | # (meaning NO caching for logged in users) 12 | if (req.http.Cookie ~ "(^|;\s*)(wp-|wordpress_)") { 13 | return (pass); 14 | } 15 | 16 | # Try a cache-lookup 17 | return (lookup); 18 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/files/etc-varnish/custom.acl.vcl: -------------------------------------------------------------------------------- 1 | acl purge { 2 | # For now, I'll only allow purges coming from localhost 3 | "127.0.0.1"; 4 | "localhost"; 5 | } 6 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/files/etc-varnish/custom.backend.vcl: -------------------------------------------------------------------------------- 1 | backend default { 2 | .host = "127.0.0.1"; 3 | .port = "8080"; 4 | .first_byte_timeout = 300s; # How long to wait before we receive a first byte from our backend? 5 | .connect_timeout = 5s; # How long to wait for a backend connection? 6 | .between_bytes_timeout = 2s; # How long to wait between bytes received from our backend? 7 | } 8 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/files/remoteip.conf: -------------------------------------------------------------------------------- 1 | # Declare header field to be parsed for end user IP address 2 | RemoteIPHeader X-Forwarded-For 3 | # Declare client IP address trusted to present RemoteIPHeader value 4 | RemoteIPInternalProxy 127.0.0.1 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: restart varnish 3 | service: name=varnish state=restarted 4 | become: true 5 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - apache 4 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Varnish packages 3 | apt: pkg={{ varnish_packages }} state=present 4 | become: true 5 | 6 | - name: Create Varnish directory structure 7 | file: path={{ item }} state=directory mode=0644 8 | become: true 9 | with_items: 10 | - /etc/varnish 11 | - /etc/varnish/conf.d 12 | - /etc/varnish/conf.d/fetch 13 | - /etc/varnish/conf.d/receive 14 | 15 | - name: Copy Varnish configuration files 16 | copy: src=etc-varnish/{{ item }} dest=/etc/varnish/{{ item }} mode=0644 17 | become: true 18 | with_items: 19 | - conf.d/fetch/wordpress.vcl 20 | - conf.d/receive/wordpress.vcl 21 | - custom.acl.vcl 22 | - custom.backend.vcl 23 | - production.vcl 24 | 25 | - name: Copy Varnish script 26 | copy: src=etc-default-varnish dest=/etc/default/varnish mode=0644 27 | become: true 28 | notify: 29 | - restart apache 30 | - restart varnish 31 | 32 | - stat: path=/etc/apache2/mods-enabled/rpaf.load 33 | register: rpaf_load 34 | 35 | - name: Disable apache module "rpaf" (if installed) 36 | command: a2dismod rpaf 37 | when: rpaf_load.stat.exists 38 | become: true 39 | notify: restart apache 40 | 41 | - name: Configure apache module "remoteip" 42 | copy: src=remoteip.conf dest=/etc/apache2/mods-available/remoteip.conf mode=0644 43 | become: true 44 | 45 | - name: Enable apache module "remoteip" 46 | command: a2enmod remoteip 47 | become: true 48 | notify: restart apache 49 | 50 | - name: Change Apache NameVirtualHost 80 => 8080 (2.2) 51 | lineinfile: regexp='^NameVirtualHost' line='NameVirtualHost *:8080' dest=/etc/apache2/ports.conf backup=yes 52 | notify: 53 | - restart apache 54 | - restart varnish 55 | when: apache_version.stdout == '2.2' 56 | become: true 57 | 58 | - name: Change Apache Listen 80 => 8080 59 | lineinfile: regexp='^Listen' line='Listen 8080' dest=/etc/apache2/ports.conf backup=yes 60 | notify: 61 | - restart apache 62 | - restart varnish 63 | become: true 64 | 65 | - name: Change VirtualHosts from 80 => 8080 66 | lineinfile: regexp='^' line='' dest=/etc/apache2/sites-available/{{ "%03d" | format(item.key_1) }}-{{ item.value }}.{{ domain }}.conf 67 | with_nested_dict: "{{ apache_vhosts }}" 68 | when: item.key_0 == stage 69 | notify: 70 | - restart apache 71 | - restart varnish 72 | become: true 73 | -------------------------------------------------------------------------------- /lib/ansible/roles/varnish/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | role_varnish: true 3 | 4 | varnish_packages: 5 | - varnish 6 | -------------------------------------------------------------------------------- /lib/ansible/roles/wp-cli/tasks/main.yml: -------------------------------------------------------------------------------- 1 | # see evolution/wordpress#126 2 | - name: Fetch latest wp-cli (on control machine) 3 | local_action: get_url dest=/tmp/wp-cli url=https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar force=yes 4 | 5 | - name: Copy latest wp-cli to remote 6 | copy: src=/tmp/wp-cli dest=/usr/local/bin/wp force=yes mode=0755 owner=deploy group=deploy 7 | become: true 8 | 9 | - name: Ensure deploy can both write and execute wp-cli 10 | file: path=/usr/local/bin group=deploy mode=0775 11 | become: true 12 | -------------------------------------------------------------------------------- /lib/capistrano/bootstrap.rake: -------------------------------------------------------------------------------- 1 | # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. 2 | Dir.glob(File.expand_path(File.dirname(__FILE__)) + '/tasks/*.rake').each { |r| import r } 3 | 4 | # Infer Git repository from current repo 5 | set :repo_url, proc { `git config --get remote.origin.url`.strip! } 6 | 7 | # This is the fastest way to deploy, once the branch is live 8 | set :deploy_via, :remote_cache 9 | 10 | # Files will be owned by deploy:www-data 11 | set :group_writable, true 12 | 13 | # Keep 1 previous releases, used with deploy:cleanup 14 | set :keep_releases, 1 15 | 16 | # Set default user & publickey for deployment 17 | set :user, "deploy" 18 | set :ssh_options, { 19 | keys: %w(./lib/ansible/files/ssh/id_rsa), 20 | forward_agent: true, 21 | auth_methods: %w(publickey), 22 | } 23 | 24 | # Auto-detect DB_* constants from wp-config.php 25 | set :wp_config, Hash[File 26 | .read('./web/wp-config.php') 27 | .scan(/DB_(\w+)(?:'|"),[^\'\"]+(?:'|")([^\'\"]*)/) 28 | .map { |match| [match[0].downcase, match[1]] } 29 | ] 30 | 31 | # Path for wp-cli on local vagrant 32 | set :local_wp_path, "/vagrant/web/wp" 33 | 34 | # fix permissions before deployment 35 | namespace :deploy do 36 | before :starting, :reconcile_git_remotes do 37 | bare_repo_path = "#{deploy_to}/repo" 38 | 39 | on roles(:web) do |host| 40 | repo_exists = test "[ -f #{bare_repo_path}/HEAD ]" 41 | 42 | if repo_exists 43 | server_remote = '' 44 | within bare_repo_path do 45 | server_remote = capture(:git, :config, '--get', 'remote.origin.url') 46 | end 47 | 48 | if fetch(:repo_url) != server_remote 49 | execute :rm, '-rf', bare_repo_path 50 | end 51 | end 52 | end 53 | end 54 | 55 | before :started, :release_permissions do 56 | invoke "evolve:permissions" 57 | end 58 | 59 | after :finished, :release_permissions do 60 | invoke "evolve:permissions" 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/db.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | namespace :db do 3 | task :prepare, :source, :target do |task, args| 4 | def filter_stage(stage) 5 | if stage == 'production' 6 | return (fetch(:www) ? 'www.' : '') + fetch(:domain) 7 | else 8 | return stage + '.' + fetch(:domain) 9 | end 10 | end 11 | 12 | stage_source = args[:source].to_s 13 | stage_target = args[:target].to_s 14 | 15 | invoke "evolve:calc_wp_path", stage_target == 'local' 16 | 17 | set :wp_cmd, [ 18 | "\"://#{filter_stage(stage_source)}/\"", 19 | "\"://#{filter_stage(stage_target)}/\"", 20 | "--path=\"#{fetch(:working_wp_path)}\"", 21 | "--url=\"http://#{stage_target}.#{fetch(:domain)}/\"", 22 | ].join(' ') 23 | end 24 | 25 | desc "Create backup of remote DB" 26 | task :backup, :skip_download do |task, args| 27 | set :db_backup_file, DateTime.now.strftime("#{fetch(:wp_config)['name']}.%Y-%m-%d.%H%M%S.sql") 28 | set :db_gzip_file, "#{fetch(:db_backup_file)}.gz" 29 | 30 | invoke "evolve:calc_wp_path", false 31 | 32 | on release_roles(:db) do |host| 33 | backups_path = "#{deploy_to}/backups" 34 | execute :mkdir, '-p', backups_path 35 | 36 | within backups_path do 37 | execute :wp, :db, :export, fetch(:db_backup_file), "--opt", "--path=\"#{fetch(:working_wp_path)}\"", "--url=\"http://#{fetch(:stage)}.#{fetch(:domain)}/\"" 38 | execute :gzip, fetch(:db_backup_file) 39 | 40 | if args[:skip_download] 41 | backups = capture(:ls, '-x', backups_path).split 42 | if backups.count >= fetch(:keep_releases) 43 | info 'Keeping %s of %s database backups on %s' % [ fetch(:keep_releases), backups.count, host.to_s ] 44 | files = (backups - backups.last(fetch(:keep_releases))) 45 | if files.any? 46 | files_str = files.map do |backup| 47 | backups_path + '/' + backup 48 | end.join(' ') 49 | execute :rm, '-rf', files_str 50 | else 51 | info 'No old database backups (keeping newest %s) on %s' % [ fetch(:keep_releases), host.to_s ] 52 | end 53 | end 54 | else 55 | download! "#{backups_path}/#{fetch(:db_gzip_file)}", fetch(:db_gzip_file) 56 | execute :rm, fetch(:db_gzip_file) 57 | end 58 | 59 | end 60 | end 61 | end 62 | 63 | task :import, :sql_file, :source_stage, :target_stage do |task, args| 64 | source_stage = args[:source_stage].to_s 65 | target_stage = args[:target_stage].to_s 66 | # if crossing stages, invoke prepare task to build wp search-replace command 67 | cross_stage = target_stage != source_stage 68 | if cross_stage 69 | invoke "evolve:db:prepare", source_stage, target_stage 70 | # otherwise just invoke calc_wp_path task to...calculate working wp path 71 | else 72 | invoke "evolve:calc_wp_path", target_stage == 'local' 73 | end 74 | if target_stage == "local" 75 | run_locally do 76 | sql_file = args[:sql_file] 77 | if sql_file.start_with?(Dir.pwd) 78 | sql_file.sub!(Dir.pwd, '/vagrant') 79 | end 80 | execute :vagrant, :up 81 | execute :vagrant, :ssh, :local, "-c 'cd /vagrant && mysql -uroot -D \"#{fetch(:wp_config)['name']}_local\" < #{sql_file}'" 82 | if cross_stage 83 | execute :vagrant, :ssh, :local, "-c 'cd #{fetch(:working_wp_path)} && wp search-replace #{fetch(:wp_cmd)}'" 84 | end 85 | end 86 | else 87 | on release_roles(:db) do 88 | within fetch(:working_wp_path) do 89 | execute :wp, :db, :import, args[:sql_file], "--path=\"#{fetch(:working_wp_path)}\"", "--url=\"http://#{fetch(:stage)}.#{fetch(:domain)}/\"" 90 | if cross_stage 91 | execute :wp, :'search-replace', fetch(:wp_cmd) 92 | end 93 | end 94 | end 95 | end 96 | end 97 | 98 | task :down do |task| 99 | begin 100 | raise "Cannot sync db down from local!" if fetch(:stage).to_s == 'local' 101 | 102 | invoke "evolve:db:backup" 103 | 104 | run_locally do 105 | execute :gzip, "-d", fetch(:db_gzip_file) 106 | invoke "evolve:db:import", fetch(:db_backup_file), fetch(:stage), "local" 107 | execute :rm, fetch(:db_backup_file) 108 | end 109 | 110 | success=true 111 | ensure 112 | invoke "evolve:log", success, task.name 113 | end 114 | end 115 | 116 | task :up do |task| 117 | begin 118 | raise "Cannot sync db up to local!" if fetch(:stage).to_s == 'local' 119 | 120 | invoke "evolve:confirm", "You are about to destroy & override the \"#{fetch(:stage)}\" database!" 121 | invoke "evolve:db:backup", true 122 | 123 | run_locally do 124 | execute :vagrant, :up 125 | execute :vagrant, :ssh, :local, "-c 'cd /vagrant && mysqldump -uroot --opt \"#{fetch(:wp_config)['name']}_local\" > #{fetch(:db_backup_file)}'" 126 | execute :gzip, fetch(:db_backup_file) 127 | end 128 | 129 | on release_roles(:db) do 130 | upload! fetch(:db_gzip_file), "/tmp/#{fetch(:db_gzip_file)}" 131 | execute :gzip, "-d", "/tmp/#{fetch(:db_gzip_file)}" 132 | 133 | invoke "evolve:db:import", "/tmp/#{fetch(:db_backup_file)}", "local", fetch(:stage) 134 | 135 | execute :rm, "/tmp/#{fetch(:db_backup_file)}" 136 | end 137 | 138 | run_locally do 139 | execute :rm, fetch(:db_gzip_file) 140 | end 141 | 142 | success=true 143 | ensure 144 | invoke "evolve:log", success, task.name 145 | end 146 | end 147 | 148 | task :exec, :sql_file do |task, args| 149 | begin 150 | raise "Missing sql file!" if args[:sql_file].nil? 151 | raise "Given sql file does not exist: #{args[:sql_file]}" unless File.exist?(args[:sql_file]) 152 | sql_size = File.size(args[:sql_file]) 153 | 154 | invoke "evolve:confirm", "You are about to execute a sql file against \"#{fetch(:stage)}\" database!" 155 | 156 | on release_roles(:db) do 157 | remote_sql_file = "/tmp/" + DateTime.now.strftime("wpe-exec.%Y-%m-%d.%H%M%S.sql") 158 | upload! args[:sql_file], remote_sql_file 159 | 160 | # fetch unmodified max_allowed_packet, then crank it up for import 161 | original_map = capture(:mysql, '-uroot', '-NBe', '"SELECT @@GLOBAL.max_allowed_packet"') 162 | execute :mysql, '-uroot', '-NBe', "\"SET @@GLOBAL.max_allowed_packet=#{sql_size}\"" 163 | 164 | # import sql file, then delete it 165 | execute :mysql, '-uroot', "-D \"#{fetch(:wp_config)['name']}_#{fetch(:stage)}\"", "< #{remote_sql_file}" 166 | execute :rm, remote_sql_file 167 | 168 | # reset max_allowed_packet 169 | execute :mysql, '-uroot', '-NBe', "'SET @@GLOBAL.max_allowed_packet=#{original_map}'" 170 | end 171 | 172 | success=true 173 | ensure 174 | invoke "evolve:log", success, task.name 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/down.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | desc "Import remote DB & files to local" 3 | task :down do 4 | invoke "evolve:db:down" 5 | invoke "evolve:files:down" 6 | end 7 | 8 | namespace :down do 9 | desc "Import remote DB to local" 10 | task :db do 11 | invoke "evolve:db:down" 12 | end 13 | desc "Download remote uploads to Vagrant" 14 | task :files do 15 | invoke "evolve:files:down" 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/files.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | namespace :files do 3 | task :prepare, :path_from, :path_to, :skip_ssh do |task, args| 4 | ssh_creds = '' 5 | unless args[:skip_ssh] 6 | invoke "evolve:prepare_key" 7 | ssh_creds = "-e \"ssh -i #{fetch(:ssh_keyfile)}\"" 8 | end 9 | 10 | set :rsync_cmd, "rsync #{ssh_creds} -avvru --delete --copy-links --progress #{args[:path_from]}/ #{args[:path_to]}/" 11 | end 12 | 13 | task :import, :source_stage, :target_stage, :source_path do |task, args| 14 | source_stage = args[:source_stage].to_s 15 | target_stage = args[:target_stage].to_s 16 | 17 | # default upload paths (unless overridden via :source_path) 18 | local_uploads = "#{Dir.pwd}/web/wp-content/uploads" 19 | remote_uploads = "#{release_path}/web/wp-content/uploads" 20 | 21 | # predefine some common bool checks 22 | uploads_exist = false 23 | local_source = source_stage == 'local' 24 | local_target = target_stage == 'local' 25 | 26 | # determine source and target upload paths, given args and stages 27 | source_path = args[:source_path] ? args[:source_path] : (local_source ? local_uploads : remote_uploads) 28 | target_path = local_target ? local_uploads : remote_uploads 29 | 30 | # predefine source path existence check 31 | uploads_test = "[ -d #{source_path} ]" 32 | 33 | # test uploads locally/remotely 34 | if (args[:source_path] && local_target) || (!args[:source_path] && local_source) 35 | run_locally do 36 | uploads_exist = test uploads_test 37 | end 38 | else 39 | on roles(:web) do |host| 40 | uploads_exist = test uploads_test 41 | end 42 | end 43 | 44 | # without uploads, bail early from this task 45 | unless uploads_exist 46 | warn "!! No uploads to rsync from #{source_path}...skipping" 47 | next 48 | end 49 | 50 | # without a source path override, wrap non-local path with "user@host:" ssh creds 51 | unless args[:source_path] 52 | ssh_host = '' 53 | on roles(:web) do |host| 54 | ssh_host = host 55 | end 56 | if local_source 57 | target_path = "#{fetch(:user)}@#{ssh_host}:#{target_path}" 58 | else 59 | source_path = "#{fetch(:user)}@#{ssh_host}:#{source_path}" 60 | end 61 | end 62 | 63 | # fix remote permissions before sync 64 | invoke "evolve:permissions" unless local_target 65 | 66 | # derive :skip_ssh from whether :source_path is defined 67 | invoke "evolve:files:prepare", source_path, target_path, args[:source_path] 68 | 69 | # execute rsync against the appropriate environment (control or remote) 70 | if (args[:source_path] && local_target) || !args[:source_path] 71 | run_locally do 72 | execute fetch(:rsync_cmd) 73 | end 74 | else 75 | on roles(:web) do |host| 76 | execute fetch(:rsync_cmd) 77 | end 78 | end 79 | 80 | # fix again after sync 81 | invoke "evolve:permissions" unless local_target 82 | 83 | end 84 | 85 | task :down do |task| 86 | begin 87 | raise "Cannot sync files down from local!" if fetch(:stage).to_s == 'local' 88 | 89 | invoke "evolve:files:import", fetch(:stage), 'local' 90 | 91 | success=true 92 | ensure 93 | invoke "evolve:log", success, task.name 94 | end 95 | end 96 | 97 | task :up do |task| 98 | begin 99 | raise "Cannot sync files up to local!" if fetch(:stage).to_s == 'local' 100 | 101 | invoke "evolve:confirm", "You are about to overwrite \"#{fetch(:stage)}\" files!" 102 | invoke "evolve:files:import", 'local', fetch(:stage) 103 | 104 | success=true 105 | ensure 106 | invoke "evolve:log", success, task.name 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/provision.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | desc "Provisions remote machine via Ansible" 3 | task :provision do |task| 4 | begin 5 | invoke "evolve:prepare_key" 6 | run_locally do 7 | def ansible_execute(command) 8 | puts "Running " + command.yellow + " on " + "localhost".blue 9 | return system(command) 10 | end 11 | 12 | ansible_path = Dir.pwd + "/lib/ansible" 13 | 14 | galaxy_reqs = "#{ansible_path}/galaxy.yml" 15 | if File.exists?(galaxy_reqs) 16 | unless File.read(galaxy_reqs).strip.empty? 17 | system("ansible-galaxy install -r #{galaxy_reqs} --force") 18 | end 19 | end 20 | 21 | play = "ansible-playbook -e stage=#{fetch(:stage)}" 22 | 23 | success = ansible_execute("#{play} --user=#{fetch(:user)} --private-key=#{ansible_path}/files/ssh/id_rsa #{ansible_path}/provision.yml") 24 | 25 | unless success 26 | error "\n\nUnable to provision with SSH publickey for \"#{fetch(:user)}\" user" 27 | 28 | set :provision_user, ask('user to provision as', 'root') 29 | 30 | ansible_execute("#{play} --user=#{fetch(:provision_user)} --ask-pass --ask-sudo-pass #{ansible_path}/user.yml") 31 | ansible_execute("#{play} --user=#{fetch(:user)} --private-key=#{ansible_path}/files/ssh/id_rsa #{ansible_path}/provision.yml") 32 | end 33 | end 34 | 35 | success=true 36 | ensure 37 | invoke "evolve:log", success, task.name 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/server.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | task :service, :action do |task, args| 3 | on release_roles(:web) do 4 | execute :sudo, "service evolution-wordpress #{args[:action]}" 5 | end 6 | end 7 | 8 | desc "Stop installed evolution web services" 9 | task :stop do 10 | invoke "evolve:service", "stop" 11 | end 12 | 13 | desc "Start installed evolution web services" 14 | task :start do 15 | invoke "evolve:service", "start" 16 | end 17 | 18 | desc "Restart installed evolution web services" 19 | task :restart do 20 | invoke "evolve:service", "restart" 21 | end 22 | 23 | desc "Reboot provisioned server" 24 | task :reboot do 25 | invoke "evolve:service", "reboot" 26 | end 27 | 28 | desc "Return provisioned evolution version" 29 | task :version do 30 | invoke "evolve:service", "version" 31 | end 32 | 33 | namespace :cache do 34 | task :clear, :url_pattern do |task, args| 35 | # build url pattern, if given 36 | url_match = '' 37 | if args[:url_pattern] 38 | # ensure any single quote literals are properly escaped 39 | args[:url_pattern].gsub!(/'/) {|s| "\\'"} 40 | url_match = " && req.url ~ #{args[:url_pattern]}" 41 | end 42 | 43 | on release_roles(:web) do 44 | # forcibly ban any cache objects matching host (and optional url) 45 | execute :sudo, :varnishadm, "-T", "127.0.0.1:6082", "-S", "/etc/varnish/secret", "'ban req.http.host ~ #{fetch(:domain)}#{url_match}'" 46 | # restart the varnish service, to clean up any banned/invalidated cache objects 47 | execute :sudo, :service, :varnish, :restart 48 | end 49 | end 50 | end 51 | 52 | namespace :logs do 53 | namespace :apache do 54 | task :tail, :action do |task, args| 55 | on release_roles(:web) do 56 | execute :sudo, "tail -f /var/log/apache2/#{fetch(:stage)}.#{fetch(:domain)}-#{args[:action]}.log" 57 | end 58 | end 59 | 60 | desc "Tail apache access log" 61 | task :access do 62 | invoke "evolve:logs:apache:tail", "access" 63 | end 64 | 65 | desc "Tail apache error log" 66 | task :error do 67 | invoke "evolve:logs:apache:tail", "error" 68 | end 69 | 70 | desc "Pull remote apache logs for analysis" 71 | task :sync do |task| 72 | begin 73 | raise "Cannot sync logs down from local!" if fetch(:stage) == 'local' 74 | 75 | log_dir = '/var/log/apache2' 76 | log_sync_prefix = 'apache2-remote-access.log' 77 | log_gzip_file = DateTime.now.strftime("#{log_sync_prefix}.%Y-%m-%d.%H%M%S.tar.gz") 78 | 79 | on release_roles(:db) do 80 | logfiles = capture(:sudo, :bash, '-c', "'cd #{log_dir} && ls -x ./*#{fetch(:domain)}-access.log*'").split 81 | raise "No matching logs in #{fetch(:stage)}, to sync down" if logfiles.empty? 82 | 83 | execute :sudo, :bash, '-c', "'cd #{log_dir} && tar -czvf /tmp/#{log_gzip_file} #{logfiles.join(' ')}'" 84 | download! "/tmp/#{log_gzip_file}", "#{log_gzip_file}" 85 | execute :sudo, :rm, "/tmp/#{log_gzip_file}" 86 | end 87 | 88 | run_locally do 89 | execute :mkdir, '-p', "./#{log_sync_prefix}/" 90 | execute :tar, '-C', "./#{log_sync_prefix}/", '-xvzf', log_gzip_file 91 | execute :vagrant, :up 92 | execute :vagrant, :ssh, :local, "-c 'sudo /usr/share/awstats/tools/logresolvemerge.pl /vagrant/#{log_sync_prefix}/* > /tmp/#{log_sync_prefix}'" 93 | execute :rm, '-rf', "./#{log_sync_prefix}/" 94 | execute :rm, log_gzip_file 95 | execute :vagrant, :ssh, :local, "-c 'sudo /usr/lib/cgi-bin/awstats.pl -config=#{fetch(:domain)} -update'" 96 | execute :vagrant, :ssh, :local, "-c 'sudo rm /tmp/#{log_sync_prefix}'" 97 | end 98 | 99 | invoke "evolve:launch_browser", "http://local.#{fetch(:domain)}/awstats/awstats.pl" 100 | 101 | success=true 102 | ensure 103 | invoke "evolve:log", success, task.name 104 | end 105 | end 106 | end 107 | 108 | desc "View varnishlog" 109 | task :varnish do 110 | on release_roles(:web) do 111 | execute :sudo, "varnishlog" 112 | end 113 | end 114 | 115 | desc "Tail pound syslog" 116 | task :pound do 117 | on release_roles(:web) do 118 | execute :sudo, "tail -f /var/log/syslog | grep --line-buffered 'pound:'" 119 | end 120 | end 121 | 122 | desc "Tail evolution log" 123 | task :evolution do 124 | on release_roles(:web) do 125 | execute :sudo, "tail -f /var/log/evolution/wordpress.log" 126 | end 127 | end 128 | end 129 | 130 | desc "Fix remote filesystem permissions" 131 | task :permissions do 132 | on release_roles(:web) do 133 | if test :ls, "#{deploy_to}/releases/*/web" 134 | # Ensure directories are group-owned by apache & group executable; SGID 135 | execute :sudo, "find -L #{deploy_to}/releases/*/web -type d -exec chown :www-data {} \\; -exec chmod 775 {} \\; -exec chmod g+s {} \\;" 136 | # Ensure files are group readable 137 | execute :sudo, "find -L #{deploy_to}/releases/*/web -type f -exec chmod 664 {} \\;" 138 | 139 | if test :ls, "#{deploy_to}/releases/*/web/wp-content" 140 | # Ensure upload directory is owned by deploy and writeable by Apache 141 | execute :sudo, "find -L #{deploy_to}/releases/*/web/wp-content -type d -exec chown deploy:www-data {} \\;" 142 | execute :sudo, "find -L #{deploy_to}/releases/*/web/wp-content/uploads/ -type d -exec chmod g+w {} \\;" 143 | end 144 | end 145 | end 146 | Rake::Task['evolve:permissions'].reenable 147 | end 148 | 149 | desc "Remove remote deployments" 150 | task :teardown do |task| 151 | begin 152 | invoke "evolve:confirm", "You are about to permanently remove everything within #{deploy_to}" 153 | 154 | on release_roles(:web) do 155 | execute :sudo, "rm -rf #{deploy_to}" 156 | end 157 | 158 | success=true 159 | ensure 160 | invoke "evolve:log", success, task.name 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/ssh.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | desc "SSH into remote machine" 3 | task :ssh do |host| 4 | invoke "evolve:prepare_key" 5 | 6 | on roles(:web) do |host| 7 | system("ssh #{fetch(:user)}@#{host} -i #{fetch(:ssh_keyfile)}") 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/up.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | desc "Export local DB & files to remote" 3 | task :up do 4 | invoke "evolve:db:up" 5 | invoke "evolve:files:up" 6 | end 7 | 8 | namespace :up do 9 | desc "Export local DB to remote" 10 | task :db do 11 | invoke "evolve:db:up" 12 | end 13 | desc "Uploads local uploads to remote" 14 | task :files do 15 | invoke "evolve:files:up" 16 | invoke "evolve:permissions" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/update.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | task :update, :flags do |task, args| 3 | invoke "evolve:calc_wp_path", fetch(:stage) == 'local' 4 | 5 | # updater binary 6 | wp_updater = '/usr/local/bin/wp-update' 7 | 8 | # handle supplemental flags 9 | majority = args[:flags].downcase.include?('major') ? '--major' : '' 10 | plugins = args[:flags].downcase.include?('plugin') ? '--skip-plugins' : '' 11 | themes = args[:flags].downcase.include?('theme') ? '--skip-themes' : '' 12 | 13 | on roles(:web) do |host| 14 | updater_exists = test "[ -f #{wp_updater} ]" 15 | raise "Missing wp-update binary! You may need to reprovision this server" unless updater_exists 16 | 17 | execute wp_updater, '--verbose', majority, plugins, themes, fetch(:working_wp_path), "http://#{fetch(:stage)}.#{fetch(:domain)}/" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/util.rake: -------------------------------------------------------------------------------- 1 | namespace :evolve do 2 | task :launch_browser, :url do |task, args| 3 | require 'launchy' 4 | Launchy.open(args[:url]) do |exception| 5 | puts "Failed to open #{args[:url]} because #{exception}" 6 | end 7 | end 8 | 9 | task :calc_wp_path, :is_local do |task, args| 10 | # short circuit when local wp_path is needed 11 | if args[:is_local] 12 | set :working_wp_path, fetch(:local_wp_path) 13 | else 14 | # fetch branch and initial wp_path 15 | branch = fetch(:branch).to_s 16 | wp_path = fetch(:wp_path).to_s 17 | 18 | on release_roles(:db) do 19 | # test wp path exists and correct as necessary 20 | unless branch == 'master' 21 | unless test "[ -d #{wp_path} ]" 22 | wp_path.sub!("/#{branch}/", '/master/') 23 | end 24 | end 25 | 26 | raise "WP path '#{wp_path}' does not exist on #{fetch(:stage)}" unless test "[ -d #{wp_path} ]" 27 | end 28 | 29 | set :working_wp_path, wp_path 30 | end 31 | Rake::Task['evolve:calc_wp_path'].reenable 32 | end 33 | 34 | task :confirm, :message do |task, args| 35 | # interpret env values of blank, "false", or "0" as falsey 36 | if ENV.has_key?('evolution_non_interactive') 37 | do_prompt = !! (ENV['evolution_non_interactive'] =~ /^(false|0)?$/i) 38 | else 39 | do_prompt = true 40 | end 41 | 42 | if do_prompt 43 | fence = "=" * (args[:message].length + 20) 44 | warn <<-WARN 45 | 46 | #{fence} 47 | 48 | WARNING: #{args[:message]} 49 | 50 | #{fence} 51 | 52 | WARN 53 | 54 | ask :confirmation, "Enter YES to continue" 55 | if fetch(:confirmation) != 'YES' then 56 | warn "Aborted!" 57 | exit 58 | end 59 | 60 | Rake::Task['evolve:confirm'].reenable 61 | end 62 | end 63 | 64 | task :retrieve_groupvars do |task| 65 | require 'yaml' 66 | set :group_vars, YAML.load_file('lib/ansible/group_vars/all') 67 | end 68 | 69 | task :log, :success, :message do |task, args| 70 | require 'etc' 71 | require 'socket' 72 | require 'shellwords' 73 | 74 | logdir = '/var/log/evolution' 75 | filename = 'wordpress.log' 76 | logfile = logdir + '/' + filename 77 | status = args[:success] ? 'Success' : 'Failure' 78 | stamp = DateTime.now.strftime('%c') 79 | user = Etc.getlogin + '@' + Socket.gethostname 80 | 81 | message = [ stamp, user, args[:message], status ].join("\t") 82 | 83 | on roles(:web) do |host| 84 | unless test "[ -f #{logfile} ] && [ $(stat -c %U #{logfile}) == '#{fetch(:user)}' ]" 85 | execute :sudo, :mkdir, '-p', logdir 86 | execute :sudo, :chown, '-R', fetch(:user), logdir 87 | execute :touch, '-a', logfile 88 | end 89 | 90 | execute :echo, Shellwords.escape(message), '>>', logfile 91 | end 92 | 93 | Rake::Task['evolve:log'].reenable 94 | end 95 | 96 | task :prepare_key do 97 | set :ssh_keyfile, fetch(:ssh_options)[:keys].last 98 | 99 | run_locally do 100 | execute "chmod 600 #{fetch(:ssh_keyfile)}" 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/wp.rake: -------------------------------------------------------------------------------- 1 | namespace :wp do 2 | desc "Execute WP-CLI command remotely" 3 | rule /^wp\:/ do |task| 4 | begin 5 | invoke "evolve:calc_wp_path", fetch(:stage) == 'local' 6 | 7 | on release_roles(:db) do 8 | within fetch(:working_wp_path) do 9 | execute "#{task.name.split(":").join(" ")} --path=\"#{fetch(:working_wp_path)}\" --url=\"http://#{fetch(:stage)}.#{fetch(:domain)}/\"" 10 | end 11 | end 12 | 13 | success=true 14 | ensure 15 | invoke "evolve:log", success, task.name 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/yeoman/bin/postinstall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var util = require('util'); 6 | 7 | var cwd = process.cwd(); 8 | var from = path.join(cwd, 'bower_components', 'wordpress'); 9 | var to = path.join(cwd, 'web', 'wp'); 10 | 11 | console.log('Evolution WordPress - Bower `postinstall` Script\n'); 12 | 13 | // Ensure `bower_components/wordpress` has been installed 14 | if (!fs.existsSync(from)) { 15 | throw new Error(util.format('WordPress has not been installed to `%s`', from)); 16 | } 17 | 18 | var toThemeDir = path.join(cwd, 'web', 'wp-content', 'themes'); 19 | 20 | if (!fs.existsSync(toThemeDir)) { 21 | var parts = path.relative(cwd, toThemeDir).split(path.sep); 22 | var toThemePartDir = cwd; 23 | 24 | while (parts.length) { 25 | toThemePartDir = path.join(toThemePartDir, parts.shift()); 26 | 27 | console.info('Creating `%s`...', toThemePartDir); 28 | fs.mkdirSync(toThemePartDir); 29 | } 30 | } 31 | 32 | var fromThemeDir = path.join(from, 'wp-content', 'themes'); 33 | var latestTheme = null; 34 | var themeExists = function (parents, name) { 35 | var exists = false; 36 | parents.forEach (function (directory) { 37 | if (fs.existsSync( path.join(directory, name) )) { 38 | exists = true; 39 | } 40 | }); 41 | return exists; 42 | }; 43 | var words = function (num) { 44 | var ones = ['','one','two','three','four','five','six','seven','eight','nine']; 45 | var tens = ['','','twenty','thirty','forty','fifty','sixty','seventy','eighty','ninety']; 46 | var teens = ['ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen']; 47 | 48 | if (num == 0) return 'zero'; 49 | else if (num < 10) return ones[num]; 50 | else if (num >= 10 && num < 20) return teens[ num - 10 ]; 51 | else { 52 | return tens[ Math.floor(num/10) ] + ones[ num % 10 ]; 53 | } 54 | } 55 | 56 | for (var testYear = new Date().getFullYear(); testYear > 2009; testYear--) { 57 | var testName = words(testYear.toString().substr(0,2)) 58 | + words(testYear.toString().substr(2,2)) 59 | ; 60 | 61 | if (themeExists([toThemeDir,fromThemeDir], testName)) { 62 | latestTheme = testName; 63 | break; 64 | } 65 | else { 66 | console.log('No theme ' + testName); 67 | } 68 | } 69 | 70 | if (latestTheme === null) { 71 | if (themeExists([fromThemeDir], 'default')) { 72 | latestTheme = 'default'; 73 | } 74 | else { 75 | throw new Error(util.format('Could not find WordPress core theme in `%s`', fromThemeDir)); 76 | } 77 | } 78 | 79 | if (themeExists([toThemeDir], latestTheme)) { 80 | console.log('Existing core theme ' + latestTheme); 81 | } 82 | else { 83 | console.log('Found theme ' + latestTheme); 84 | 85 | var fromTheme = path.join(fromThemeDir, latestTheme); 86 | var toTheme = path.join(toThemeDir, latestTheme); 87 | 88 | console.log('Moving theme `%s` to `%s`...', path.relative(cwd, fromTheme), path.relative(cwd, toTheme)); 89 | fs.renameSync(fromTheme, toTheme); 90 | } 91 | 92 | // Remove existing `web/wp` folder 93 | if (fs.existsSync(to)) { 94 | console.info('Removing `%s`...', to); 95 | 96 | var removeDir = function(dir) { 97 | var nodes = fs.readdirSync(dir).map(function(node) { 98 | return path.join(dir, node); 99 | }); 100 | 101 | nodes.forEach(function(node) { 102 | var stats = fs.lstatSync(node); 103 | 104 | if (stats.isDirectory()) { 105 | removeDir(node); 106 | } else { 107 | fs.unlinkSync(node); 108 | } 109 | }); 110 | 111 | fs.rmdirSync(dir); 112 | }; 113 | 114 | removeDir(to); 115 | } 116 | 117 | console.info('Renaming `%s` to `%s`...', path.relative(cwd, from), path.relative(cwd, to)); 118 | fs.renameSync(from, to); 119 | 120 | console.log('Done!'); 121 | -------------------------------------------------------------------------------- /lib/yeoman/templates/Capfile: -------------------------------------------------------------------------------- 1 | set :deploy_config_path, 'lib/capistrano/deploy.rb' 2 | set :stage_config_path, 'lib/capistrano/deploy' 3 | 4 | # Load DSL and Setup Up Stages 5 | require 'capistrano/setup' 6 | 7 | # Includes default deployment tasks 8 | require 'capistrano/deploy' 9 | 10 | # Includes tasks from other gems included in your Gemfile 11 | # 12 | # For documentation on these, see for example: 13 | # 14 | # https://github.com/capistrano/rvm 15 | # https://github.com/capistrano/rbenv 16 | # https://github.com/capistrano/chruby 17 | # https://github.com/capistrano/bundler 18 | # https://github.com/capistrano/rails 19 | # 20 | # require 'capistrano/rvm' 21 | # require 'capistrano/rbenv' 22 | # require 'capistrano/chruby' 23 | # require 'capistrano/bundler' 24 | # require 'capistrano/rails/assets' 25 | # require 'capistrano/rails/migrations' 26 | 27 | # Load Evolution WordPress' tasks 28 | load File.expand_path(File.dirname(__FILE__)) + '/bower_components/evolution-wordpress/lib/capistrano/bootstrap.rake' 29 | 30 | # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. 31 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 32 | -------------------------------------------------------------------------------- /lib/yeoman/templates/Evolution.php: -------------------------------------------------------------------------------- 1 | '; 6 | 7 | public static $hostname; 8 | public static $env; 9 | 10 | public static function getDbName($name) 11 | { 12 | return sprintf('%s_%s', $name, static::getEnv()); 13 | } 14 | 15 | public static function getHostname() 16 | { 17 | if (isset(self::$hostname)) { 18 | return self::$hostname; 19 | } 20 | 21 | $env = self::getEnv(); 22 | 23 | if (in_array($env, array('local', 'staging'))) { 24 | self::$hostname = $env.'.'.self::DOMAIN; 25 | } 26 | else { 27 | $content = file_get_contents(dirname(__FILE__).'/lib/ansible/group_vars/all'); 28 | 29 | if (preg_match('/www[ ]*:[ ]*(true)/', $content)) { 30 | self::$hostname = 'www.'.self::DOMAIN; 31 | } 32 | else { 33 | self::$hostname = self::DOMAIN; 34 | } 35 | } 36 | 37 | return self::$hostname; 38 | } 39 | 40 | public static function getEnv() 41 | { 42 | if (isset(self::$env)) { 43 | return self::$env; 44 | } 45 | 46 | preg_match('/(?:(local|staging|production)\.)?'.preg_quote(self::DOMAIN).'$/', $_SERVER['SERVER_NAME'], $matches); 47 | 48 | $match = count($matches) > 1 ? array_pop($matches) : 'production'; 49 | $known = array('local', 'staging', 'production'); 50 | 51 | self::$env = in_array($match, $known) ? $match : 'production'; 52 | 53 | return self::$env; 54 | } 55 | 56 | public static function initEnv() 57 | { 58 | // Set environment to the last sub-domain (e.g. foo.staging.site.com => 'staging') 59 | if (!defined('WP_ENV')) { 60 | define('WP_ENV', Evolution::getEnv()); 61 | } 62 | 63 | return WP_ENV; 64 | } 65 | 66 | public static function rewriteUrls() 67 | { 68 | if (!function_exists('is_blog_installed') || !is_blog_installed()) { 69 | return false; 70 | } 71 | 72 | $old_url = '://' . self::getHostname(); 73 | $new_url = '://' . htmlspecialchars($_SERVER['HTTP_HOST']); 74 | 75 | if ($old_url === $new_url) { 76 | return false; 77 | } 78 | 79 | // Remove domain from uploads 80 | update_option('upload_path', null); 81 | 82 | // Ensure internal WordPress functions map correctly to new url (but don't want to persist in the DB) 83 | add_filter('option_home', function($value) use ($old_url, $new_url) { return str_replace($old_url, $new_url, $value); }); 84 | add_filter('option_siteurl', function($value) use ($old_url, $new_url) { return str_replace($old_url, $new_url, $value); }); 85 | add_filter('option_upload_path', function($value) use ($old_url, $new_url) { return str_replace($old_url, $new_url, $value); }); 86 | add_filter('option_upload_url_path', function($value) use ($old_url, $new_url) { return str_replace($old_url, $new_url, $value); }); 87 | add_filter('wp_get_attachment_url', function($value) use ($old_url, $new_url) { return str_replace($old_url, $new_url, $value); }); 88 | 89 | // Override URLs in output with local environment URL 90 | ob_start( function( $output ) use ( $old_url, $new_url ) { 91 | return str_replace( $old_url, $new_url, $output ); 92 | } ); 93 | 94 | register_shutdown_function( function() use ( $old_url, $new_url ) { 95 | @ob_end_flush(); 96 | } ); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /lib/yeoman/templates/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'capistrano', '~> 3.2.1', require: false, group: :development 4 | gem 'colored', '~> 1.2', require: false, group: :development 5 | gem 'launchy', '~> 2.4', require: false, group: :development 6 | gem 'inquirer.rb', '~> 0.0.3', require: false, group: :development 7 | -------------------------------------------------------------------------------- /lib/yeoman/templates/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.8) 5 | capistrano (3.2.1) 6 | i18n 7 | rake (>= 10.0.0) 8 | sshkit (~> 1.3) 9 | colored (1.2) 10 | colorize (0.7.3) 11 | i18n (0.6.9) 12 | inquirer.rb (0.0.9) 13 | rainbow (~> 2.1) 14 | launchy (2.4.3) 15 | addressable (~> 2.3) 16 | net-scp (1.2.1) 17 | net-ssh (>= 2.6.5) 18 | net-ssh (2.9.1) 19 | rainbow (2.2.2) 20 | rake 21 | rake (10.3.2) 22 | sshkit (1.5.1) 23 | colorize 24 | net-scp (>= 1.1.2) 25 | net-ssh (>= 2.8.0) 26 | 27 | PLATFORMS 28 | ruby 29 | 30 | DEPENDENCIES 31 | capistrano (~> 3.2.1) 32 | colored (~> 1.2) 33 | inquirer.rb (~> 0.0.3) 34 | launchy (~> 2.4) 35 | -------------------------------------------------------------------------------- /lib/yeoman/templates/README.md: -------------------------------------------------------------------------------- 1 | [<%= props.name %>][<%= props.domain %>] 2 | <%= new Array(props.name.length + props.domain.length + 5).join('=') %> 3 | 4 | > Powered by [Evolution WordPress][evolution-wordpress] *v<%= pkg.version %>* 5 | 6 | <%= readmeFile %> 7 | [<%= props.domain %>]: http://<%= props.www ? 'www.' : '' %><%= props.domain %>/ 8 | [evolution-wordpress]: https://github.com/evolution/wordpress/ 9 | -------------------------------------------------------------------------------- /lib/yeoman/templates/Vagrantfile: -------------------------------------------------------------------------------- 1 | # Borrowed from: http://superuser.com/questions/701735/run-script-on-host-machine-during-vagrant-up#answer-992220 2 | module LocalCommand 3 | class Config < Vagrant.plugin("2", :config) 4 | attr_accessor :command 5 | end 6 | 7 | class Plugin < Vagrant.plugin("2") 8 | name "local_shell" 9 | 10 | config(:local_shell, :provisioner) do 11 | Config 12 | end 13 | 14 | provisioner(:local_shell) do 15 | Provisioner 16 | end 17 | end 18 | 19 | class Provisioner < Vagrant.plugin("2", :provisioner) 20 | def provision 21 | result = system "#{config.command}" 22 | end 23 | end 24 | end 25 | 26 | Vagrant.configure("2") do |config| 27 | # Configuring the hostmanager to automatically alter the hosts file for development testing 28 | config.hostmanager.enabled = true 29 | config.hostmanager.include_offline = true 30 | config.hostmanager.ignore_private_ip = false 31 | config.hostmanager.manage_host = true 32 | 33 | # Every Vagrant virtual environment requires a box to build off of. 34 | config.vm.box = "ubuntu/trusty64" 35 | 36 | # Borrowed from: https://stefanwrobel.com/how-to-make-vagrant-performance-not-suck 37 | config.vm.provider :virtualbox do |vb| 38 | host = RbConfig::CONFIG['host_os'] 39 | mem_divisor = 4 # Allocate 1/4 host memory 40 | 41 | if host =~ /darwin|bsd/ 42 | cpus = `sysctl -n hw.ncpu`.to_i 43 | mem = `sysctl -n hw.memsize`.to_i / 1024 / 1024 / mem_divisor 44 | elsif host =~ /linux/ 45 | cpus = `nproc`.to_i 46 | mem = `grep 'MemTotal' /proc/meminfo | sed -e 's/MemTotal://' -e 's/ kB//'`.to_i / 1024 / mem_divisor 47 | elsif host =~ /mswin|mingw/ 48 | cpus = `WMIC CPU Get NumberOfLogicalProcessors /Value`.strip.split('=')[1].to_i 49 | mem = `WMIC ComputerSystem Get TotalPhysicalMemory /Value`.strip.split('=')[1].to_i / 1024 / 1024 / mem_divisor 50 | else 51 | cpus = 1 52 | mem = 1024 53 | end 54 | 55 | vb.customize ["modifyvm", :id, "--memory", mem] 56 | vb.customize ["modifyvm", :id, "--cpus", cpus] 57 | vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 58 | end 59 | 60 | config.vm.define :local do |box| 61 | box.vm.hostname = "local.<%= props.domain %>" 62 | 63 | # Static IP for testing. 64 | box.vm.network :private_network, ip: "<%= props.ip %>" 65 | box.vm.network :forwarded_port, guest: 22, id: 'ssh', host: <%= props.ip.split('.').join('').split('0').join('').slice(-4) %>, auto_correct: true 66 | 67 | # Remount the default shared folder as NFS for caching and speed 68 | box.vm.synced_folder ".", "/vagrant", :nfs => true 69 | 70 | # Remove any known hosts when (re)provisioning 71 | box.vm.provision "ssh-cleanup", type: "local_shell", command: "(ssh-keygen -R local.<%= props.domain %> && ssh-keygen -R <%= props.ip %>) || :" 72 | 73 | # Provision local.<%= props.domain %> 74 | box.vm.provision :ansible do |ansible| 75 | galaxy_reqs = "lib/ansible/galaxy.yml" 76 | if File.exists?(galaxy_reqs) 77 | unless File.read(galaxy_reqs).strip.empty? 78 | ansible.galaxy_role_file = galaxy_reqs 79 | end 80 | end 81 | ansible.playbook = "lib/ansible/provision.yml" 82 | ansible.inventory_path = "lib/ansible/hosts" 83 | ansible.limit = "local" 84 | ansible.extra_vars = { stage: "local" } 85 | end 86 | end 87 | 88 | # Update IPs 89 | config.vm.provision :hostmanager 90 | end 91 | -------------------------------------------------------------------------------- /lib/yeoman/templates/_bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "postinstall": "./bower_components/evolution-wordpress/lib/yeoman/bin/postinstall" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/yeoman/templates/_editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.html, *.php] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /lib/yeoman/templates/_gitignore: -------------------------------------------------------------------------------- 1 | <% if (!props.private) { %># Sensitive files 2 | # Remove to version deployment keys 3 | lib/ansible/files/ssh/ 4 | # Remove to version SSL certificates & configuration 5 | lib/ansible/files/ssl/ 6 | <% } %> 7 | # Application files 8 | error_log 9 | 10 | # WordPress files 11 | **/wp-content/cache/** 12 | !**/wp-content/cache/.htaccess 13 | **/wp-content/upgrade 14 | **/wp-content/uploads 15 | 16 | # Misc 17 | *.diff 18 | *.err 19 | *.orig 20 | *.log 21 | *.rej 22 | *.swo 23 | *.swp 24 | *.vi 25 | *~ 26 | *.sass-cache 27 | *.sql 28 | *.sql.gz 29 | 30 | # OS or Editor folders 31 | .DS_Store 32 | __MACOSX 33 | Thumbs.db 34 | .cache 35 | .project 36 | .settings 37 | .tmproj 38 | *.esproj 39 | nbproject 40 | *.sublime-project 41 | *.sublime-workspace 42 | *.komodoproject 43 | .komodotools 44 | 45 | # Folders to ignore 46 | .hg 47 | .svn 48 | .CVS 49 | .idea 50 | .vagrant 51 | _notes 52 | bower_components 53 | node_modules 54 | backups 55 | web/wp 56 | lib/ansible/roles 57 | 58 | # Generated file, don't edit above this line 59 | <%= gitignoreFile %> 60 | -------------------------------------------------------------------------------- /lib/yeoman/templates/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | 3 | hostfile = ./lib/ansible/hosts 4 | remote_user = deploy 5 | roles_path = ./bower_components/evolution-wordpress/lib/ansible/roles 6 | lookup_plugins = ./bower_components/evolution-wordpress/lib/ansible/lookup_plugins 7 | 8 | [ssh_connection] 9 | control_path = %(directory)s/%%h-%%p-%%r 10 | -------------------------------------------------------------------------------- /lib/yeoman/templates/bin/import: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | IMPORTER_PATH="$( dirname "$0" )/../bower_components/evolution-wordpress" 4 | IMPORTER_SCRIPT="$IMPORTER_PATH/bin/import" 5 | 6 | chmod +x "$IMPORTER_SCRIPT" 7 | 8 | (cd "$IMPORTER_PATH"; npm install) 9 | 10 | VAGRANT_STATUS=$(vagrant status) 11 | 12 | if [[ "$VAGRANT_STATUS" == *"running ("* ]]; then 13 | $IMPORTER_SCRIPT 14 | else 15 | echo -e "Make sure your vagrant is running first! Run \033[0;32mvagrant up\033[0m" 16 | fi 17 | 18 | -------------------------------------------------------------------------------- /lib/yeoman/templates/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": <%= props.private ? 'true' : 'false' %>, 3 | "name": "<%= props.name %>", 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "evolution-wordpress": "<%= options.dev ? 'master' : '~' + pkg.version %>", 7 | "wordpress": "<%= props.wordpress %>" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/files/ssh/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/lib/yeoman/templates/lib/ansible/files/ssh/.gitkeep -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/files/ssl/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/lib/yeoman/templates/lib/ansible/files/ssl/.gitkeep -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/files/ssl/local.cfg: -------------------------------------------------------------------------------- 1 | HOME = . 2 | 3 | [req] 4 | prompt = no 5 | distinguished_name = req_distinguished_name 6 | req_extensions = v3_req 7 | x509_extensions = v3_req 8 | 9 | [req_distinguished_name] 10 | C=US 11 | ST=Texas 12 | L=Houston 13 | O=IT 14 | CN=local.<%= props.domain %> 15 | 16 | [ v3_req ] 17 | basicConstraints = CA:FALSE 18 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 19 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/files/ssl/production.cfg: -------------------------------------------------------------------------------- 1 | HOME = . 2 | 3 | [req] 4 | prompt = no 5 | distinguished_name = req_distinguished_name 6 | req_extensions = v3_req 7 | x509_extensions = v3_req 8 | 9 | [req_distinguished_name] 10 | C=US 11 | ST=Texas 12 | L=Houston 13 | O=IT 14 | CN=<%= props.domain %> 15 | 16 | [ v3_req ] 17 | basicConstraints = CA:FALSE 18 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 19 | subjectAltName = @alt_names 20 | 21 | [ alt_names ] 22 | DNS.1 = www.<%= props.domain %> 23 | DNS.2 = production.<%= props.domain %> 24 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/files/ssl/staging.cfg: -------------------------------------------------------------------------------- 1 | HOME = . 2 | 3 | [req] 4 | prompt = no 5 | distinguished_name = req_distinguished_name 6 | req_extensions = v3_req 7 | x509_extensions = v3_req 8 | 9 | [req_distinguished_name] 10 | C=US 11 | ST=Texas 12 | L=Houston 13 | O=IT 14 | CN=staging.<%= props.domain %> 15 | 16 | [ v3_req ] 17 | basicConstraints = CA:FALSE 18 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 19 | subjectAltName = @alt_names 20 | 21 | [ alt_names ] 22 | DNS.1 = *.staging.<%= props.domain %> 23 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/galaxy.yml: -------------------------------------------------------------------------------- 1 | <% if (props.datadog) { %>- src: Datadog.datadog<% } %> 2 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/group_vars/all: -------------------------------------------------------------------------------- 1 | --- 2 | evolve_version: <%= pkg.version %> 3 | domain: <%= props.domain %> 4 | www: <%= props.www %> 5 | 6 | monitoring: 7 | newrelic: '<%= props.newrelic %>' 8 | 9 | mysql: 10 | name: <%= props.DB_NAME %> 11 | user: <%= props.DB_USER %> 12 | password: <%= props.DB_PASSWORD %> 13 | host: <%= props.DB_HOST %> 14 | 15 | # custom__packages: 16 | # - "extra-apt-package" 17 | 18 | <% if (props.datadog) { %>datadog_api_key: '<%= props.datadog %>'<% } %> 19 | 20 | # php__memory_limit: 21 | # php__version_7: false 22 | 23 | # apache__start_servers: 24 | # apache__min_spare_servers: 25 | # apache__max_spare_servers: 26 | # apache__max_clients: 27 | # apache__max_requests_per_child: 28 | 29 | <% if (props.wp_autoupdate) { 30 | %>wordpress__autoupdate: <%= props.wp_autoupdate %><% 31 | } else { 32 | %># wordpress__autoupdate: false<% 33 | } %> 34 | # wordpress__autoupdate_skip_plugins: false 35 | # wordpress__autoupdate_skip_themes: false 36 | # wordpress__xmlrpc_allow: false 37 | # wordpress__xmlrpc_whitelist: 38 | # - "local" 39 | 40 | mail__postmaster: no-reply@<%= props.domain %> 41 | 42 | # iptables__ipv6: 0 43 | 44 | # fail2ban__whitelist: 45 | # - 127.0.0.1/8 46 | # fail2ban__ban_time: 600 47 | # fail2ban__notification_email: root@localhost 48 | # fail2ban__notify_on_ban: 0 49 | 50 | # swap__path: /swapfile 51 | # swap__swappiness: 10 52 | # swap__vfs_cache_pressure: 50 53 | 54 | # snapshots__method: local 55 | # snapshots__credentials: ~ 56 | # snapshots__container: /home/deploy/backup/production.<%= props.domain %> 57 | # snapshots__interval: 1d 58 | # snapshots__retention: { hours: ~, days: 1, weeks: 1, months: 1, years: 1 } 59 | # snapshots__retention_lag: true 60 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/hosts: -------------------------------------------------------------------------------- 1 | [local] 2 | local.<%= props.domain %> stage=local 3 | 4 | [production] 5 | production.<%= props.domain %> stage=production 6 | 7 | [staging] 8 | staging.<%= props.domain %> stage=staging 9 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: "{{ stage }}" # Easy execution via: `ansible-playbook provision.yml -e stage=local` 3 | gather_facts: yes # Host specs required for templates 4 | 5 | roles: 6 | - prepare # (Required) Stop any existing evolution services 7 | 8 | - common # (Required) Base configuration 9 | - user # (Required) User for SSH/Ansible/Capistrano auth 10 | - apache # (Required) Web server (Soon: Nginx) 11 | - mysql # (Required) Database server (Soon: Maria) 12 | - php # (Required) PHP 5.3 13 | - node # (Required) Tools for deployment (e.g. `bower`) 14 | - wp-cli # (Required) CLI for managing WordPress 15 | 16 | # Optional Features 17 | <% if (props.ssl) { %>- pound # (Optional) SSL support & decryption<% } %><% props.roles.map(function(role) { %> 18 | <%= role.checked ? '-' : '#' %> <%= role.value %><% }) %> 19 | <% if (props.newrelic) { %> - { role: newrelic, become: true, when: stage == 'production' } # (Optional) New Relic application/server monitoring 20 | <% } %><% if (props.datadog) { %> - { role: Datadog.datadog, become: true, when: stage == 'production' } # (Optional) Datadog monitoring support 21 | <% } %> # /Optional Features 22 | 23 | - cleanup # (Required) Generate init.d for installed evolution services 24 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/ansible/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: "{{ stage }}" # Easy execution via: `ansible-playbook provision.yml -e stage=local` 3 | gather_facts: yes 4 | 5 | roles: 6 | - user # (Required) User for SSH/Ansible/Capistrano auth 7 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/capistrano/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid only for Capistrano 3.2.1 2 | lock '3.2.1' 3 | 4 | # Infer branch (unless specified via env var) from current repo 5 | if ENV.has_key?('branch') 6 | set :branch, ENV['branch'] 7 | else 8 | matches = proc { `git branch`.match(/\* (\S+)\s/m) } 9 | set :branch, (matches.call ? matches.call[1] : "master") 10 | end 11 | 12 | # Repository name 13 | set :application, "<%= props.name %>" 14 | set :domain, "<%= props.domain %>" 15 | set :deploy_to, "/var/www/#{fetch(:domain)}/#{fetch(:stage)}/#{fetch(:branch)}" 16 | set :linked_dirs, %w{web/wp-content/uploads} 17 | set :wp_path, "#{release_path}/web/wp" 18 | set :www, <%= props.www ? 'true' : 'false' %> 19 | 20 | namespace :deploy do 21 | after :updated, :bower_install do 22 | on roles(:web) do 23 | execute "cd #{release_path} && bower install --config.interactive=false" 24 | end 25 | end 26 | after :finished, :launch_browser do 27 | subdomain = fetch(:stage).to_s 28 | branch = fetch(:branch).to_s 29 | 30 | if subdomain == 'staging' && branch != 'master' 31 | subdomain = "#{branch}.staging" 32 | end 33 | 34 | invoke "evolve:launch_browser", "http://#{subdomain}.#{fetch(:domain)}/" 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/capistrano/deploy/local.rb: -------------------------------------------------------------------------------- 1 | server 'local.<%= props.domain %>', 2 | roles: %w{db web}, 3 | user: fetch(:user), 4 | ssh_options: fetch(:ssh_options) 5 | 6 | set :wp_path, fetch(:local_wp_path) 7 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/capistrano/deploy/production.rb: -------------------------------------------------------------------------------- 1 | server 'production.<%= props.domain %>', 2 | roles: %w{db web}, 3 | user: fetch(:user), 4 | ssh_options: fetch(:ssh_options) 5 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/capistrano/deploy/staging.rb: -------------------------------------------------------------------------------- 1 | server 'staging.<%= props.domain %>', 2 | roles: %w{db web}, 3 | user: fetch(:user), 4 | ssh_options: fetch(:ssh_options) 5 | -------------------------------------------------------------------------------- /lib/yeoman/templates/lib/capistrano/tasks/_gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/lib/yeoman/templates/lib/capistrano/tasks/_gitkeep -------------------------------------------------------------------------------- /lib/yeoman/templates/web/_htaccess: -------------------------------------------------------------------------------- 1 | # BEGIN Evolution WordPress 2 | 3 | RewriteEngine On 4 | RewriteBase / 5 | 6 | # Force <%= props.www ? '' : 'non-' %>www for production 7 | RewriteCond %{HTTP_HOST} ^<%= props.www ? '' : 'www\\.' %><%= props.domain.replace('.', '\\.') %>$ [NC] 8 | RewriteCond %{HTTP:X-Forwarded-Proto} ^http(s)| [NC] 9 | RewriteRule ^(.*)$ http%1://<%= props.www ? 'www.' : '' %><%= props.domain %>/$1 [L,R=301,NC] 10 | 11 | # Prevent spidering of non-canonical URLs, such as `[*.]staging.*` and `production.*`. 12 | RewriteCond %{HTTP_HOST} (^local|staging|^production)\.<%= props.domain.replace('.', '\\.') %>$ [NC] 13 | RewriteRule ^robots.txt$ no_robots.txt [L] 14 | 15 | ## SECURITY 16 | 17 | # Block access to hidden files and directories. 18 | RewriteCond %{SCRIPT_FILENAME} -d [OR] 19 | RewriteCond %{SCRIPT_FILENAME} -f 20 | RewriteRule "(^|/)\." - [F] 21 | 22 | # Rules to protect wp-includes 23 | RewriteRule ^wp/wp-admin/includes/ - [F] 24 | RewriteRule !^wp/wp-includes/ - [S=3] 25 | RewriteCond %{SCRIPT_FILENAME} !^(.*)wp-includes/ms-files.php 26 | RewriteRule ^wp/wp-includes/[^/]+\.php$ - [F] 27 | RewriteRule ^wp/wp-includes/js/tinymce/langs/.+\.php - [F] 28 | RewriteRule ^wp/wp-includes/theme-compat/ - [F] 29 | 30 | # Rules to prevent php execution in uploads 31 | RewriteRule ^(.*)/uploads/(.*).php(.?) - [F] 32 | 33 | # Rules to block unneeded HTTP methods 34 | RewriteCond %{REQUEST_METHOD} ^(TRACE|DELETE|TRACK) [NC] 35 | RewriteRule ^(.*)$ - [F] 36 | 37 | # Rules to block foreign characters in URLs 38 | RewriteCond %{QUERY_STRING} ^.*(%0|%A|%B|%C|%D|%E|%F).* [NC] 39 | RewriteRule ^(.*)$ - [F] 40 | 41 | # Rules to help reduce spam 42 | RewriteCond %{REQUEST_METHOD} POST 43 | RewriteCond %{REQUEST_URI} ^(.*)wp-comments-post\.php* 44 | RewriteCond %{HTTP_REFERER} !^(.*)<%= props.domain %>.* 45 | RewriteCond %{HTTP_REFERER} !^http://jetpack\.wordpress\.com/jetpack-comment/ [OR] 46 | RewriteCond %{HTTP_USER_AGENT} ^$ 47 | RewriteRule ^(.*)$ - [F] 48 | 49 | # END Evolution WordPress 50 | <%= htaccessFile %> 51 | <% if (!htaccessWpBlock) { %># BEGIN WordPress 52 | 53 | RewriteEngine On 54 | RewriteBase / 55 | RewriteRule ^index\.php$ - [L] 56 | RewriteCond %{REQUEST_FILENAME} !-f 57 | RewriteCond %{REQUEST_FILENAME} !-d 58 | RewriteRule . /index.php [L] 59 | 60 | # END WordPress<% } %> 61 | -------------------------------------------------------------------------------- /lib/yeoman/templates/web/index.php: -------------------------------------------------------------------------------- 1 | /sitemap.xml 16 | -------------------------------------------------------------------------------- /lib/yeoman/templates/web/wp-config.php: -------------------------------------------------------------------------------- 1 | 50 | /** Output rewriting for production.DOMAIN or *.staging.DOMAIN **/ 51 | Evolution::rewriteUrls(); 52 | -------------------------------------------------------------------------------- /lib/yeoman/templates/web/wp-content/mu-plugins/_gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/lib/yeoman/templates/web/wp-content/mu-plugins/_gitkeep -------------------------------------------------------------------------------- /lib/yeoman/templates/web/wp-content/plugins/_gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/lib/yeoman/templates/web/wp-content/plugins/_gitkeep -------------------------------------------------------------------------------- /lib/yeoman/templates/web/wp-content/themes/_gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/lib/yeoman/templates/web/wp-content/themes/_gitkeep -------------------------------------------------------------------------------- /lib/yeoman/templates/web/wp-content/uploads/_gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evolution/wordpress/7eec722968341f4972b52a48c66da4814235ce8c/lib/yeoman/templates/web/wp-content/uploads/_gitkeep -------------------------------------------------------------------------------- /lib/yeoman/welcome.txt: -------------------------------------------------------------------------------- 1 | 2 | ,---. | | o 3 | |--- . ,,---.| . .|--- .,---.,---. 4 | | \ / | || | || || || | 5 | `---' `' `---'`---'`---'`---'``---'` ' 6 | . . . |,---. 7 | | | |,---.,---.,---||---',---.,---.,---.,---. 8 | | | || || | || | |---'`---.`---. 9 | `-'-'`---'` `---'` ` `---'`---'`---' 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "evolution-wordpress", 4 | "version": "1.10.2", 5 | "description": "Libraries for a multi-staged WordPress workflow with Vagrant", 6 | "main": "lib/yeoman/index.js", 7 | "dependencies": { 8 | "chalk": "^0.5.1", 9 | "fs-extra": "^0.20.1", 10 | "request": "^2.42.0", 11 | "ssh-keygen": "^0.2.1", 12 | "in-words": "^0.1.0", 13 | "yeoman-generator": "^0.16.0", 14 | "deasync": "^0.1.0", 15 | "inquirer": "^0.8.5", 16 | "replace": "^0.3.0", 17 | "yauzl": "^2.5.0", 18 | "glob": "^4.0.4" 19 | }, 20 | "devDependencies": { 21 | "mocha": "^1.20.1", 22 | "zombie": "^3.1.1" 23 | }, 24 | "scripts": { 25 | "test": "./bin/test" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/evolution/wordpress.git" 30 | }, 31 | "author": "Eric Clemmons ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/evolution/wordpress/issues" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./test 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Testing Evolution WordPress 2 | 3 | ### Dependencies 4 | 5 | - Composer (`$ curl -sS https://getcomposer.org/installer | php`) 6 | - Node + NPM 7 | - Vagrant 8 | 9 | ### Setup 10 | 11 | Install Composer dependencies: 12 | 13 | ```shell 14 | $ php composer.phar install --dev 15 | ``` 16 | 17 | Install NPM dependencies: 18 | 19 | ```shell 20 | $ npm install 21 | ``` 22 | 23 | ### Testing Scaffolding 24 | 25 | Generate test project scaffolding: 26 | 27 | ```shell 28 | $ ./bin/mock 29 | ``` 30 | 31 | ### Testing Provisioning 32 | 33 | Start test project server: 34 | 35 | ```shell 36 | $ (cd temp && vagrant up) 37 | ``` 38 | 39 | ### Unit Tests 40 | 41 | ```shell 42 | $ ./vendor/bin/phpunit 43 | ``` 44 | 45 | ### End-User Testing 46 | 47 | Run tests: 48 | 49 | ```shell 50 | $ npm test 51 | ``` 52 | 53 | Tests will be ran against the new entries in `/etc/hosts`: 54 | 55 | > http://local.example.com and http://example.com/ 56 | -------------------------------------------------------------------------------- /test/functional/00-firewall.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var exec = require('child_process').exec; 5 | var util = require('util'); 6 | 7 | var port_cmd; 8 | 9 | describe('firewall ports', function(done) { 10 | it('should resolve host', function(done) { 11 | if (process.env.TRAVIS) { 12 | port_cmd = 'sudo iptables -L -n | grep :%d'; 13 | done(); 14 | } else { 15 | exec('php -r "echo gethostbyname(\'local.example.com.\');"', { 16 | cwd: process.cwd() + '/temp' 17 | }, function(err, stdout, stderr) { 18 | assert.ifError(err); 19 | port_cmd = 'nc -z -w 1 ' + stdout.trim() + ' %d'; 20 | done(); 21 | }); 22 | } 23 | }); 24 | 25 | describe('should be closed', function(done) { 26 | it('mysql', function(done) { 27 | exec(util.format(port_cmd, '3306'), { 28 | cwd: process.cwd() + '/temp' 29 | }, function(err, stdout, stderr) { 30 | assert.ok(err); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('apache backend', function(done) { 36 | exec(util.format(port_cmd, '8080'), { 37 | cwd: process.cwd() + '/temp' 38 | }, function(err, stdout, stderr) { 39 | assert.ok(err); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('should be open', function(done) { 46 | it('ssh', function(done) { 47 | exec(util.format(port_cmd, '22'), { 48 | cwd: process.cwd() + '/temp' 49 | }, function(err, stdout, stderr) { 50 | assert.ifError(err); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('http', function(done) { 56 | exec(util.format(port_cmd, '80'), { 57 | cwd: process.cwd() + '/temp' 58 | }, function(err, stdout, stderr) { 59 | assert.ifError(err); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('https', function(done) { 65 | exec(util.format(port_cmd, '443'), { 66 | cwd: process.cwd() + '/temp' 67 | }, function(err, stdout, stderr) { 68 | assert.ifError(err); 69 | done(); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/functional/01-install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Browser = require('zombie'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | describe('Mock site', function() { 9 | it('may not be installed', function(done) { 10 | var browser = new Browser(); 11 | 12 | browser 13 | .visit('http://local.example.com/wp/wp-admin/install.php') 14 | .then(function() { 15 | if (browser.button('Continue')) { 16 | browser.select('language', 'English (United States)'); 17 | 18 | return browser.pressButton('Continue'); 19 | } 20 | }) 21 | .then(function() { 22 | if (browser.button('Hide')) { 23 | return browser.pressButton('Hide'); 24 | } 25 | }) 26 | .then(function() { 27 | if (browser.button('Install WordPress')) { 28 | browser 29 | .fill('Site Title', 'Evolution WordPress Test') 30 | .fill('Username', 'test') 31 | .fill('admin_password', 'test') 32 | .fill('admin_password2', 'test') 33 | .fill('admin_email', 'test@example.com') 34 | .uncheck('blog_public') 35 | ; 36 | 37 | return browser.pressButton('Install WordPress'); 38 | } 39 | }) 40 | .then(done, done) 41 | ; 42 | }); 43 | 44 | it('should be installed', function(done) { 45 | var browser = new Browser(); 46 | 47 | browser 48 | .visit('http://local.example.com/wp/wp-admin/install.php') 49 | .then(function() { 50 | assert.equal('Log In', browser.text('a.button')); 51 | }) 52 | .then(done, done) 53 | ; 54 | }) 55 | }); 56 | -------------------------------------------------------------------------------- /test/functional/02-wp.rewrite.structure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var exec = require('child_process').exec; 5 | 6 | describe('cap local wp:rewrite:structure:/%postname%/', function(done) { 7 | it('should not fail', function(done) { 8 | exec('bundle exec cap local wp:rewrite:structure:/%postname%/', { 9 | cwd: process.cwd() + '/temp' 10 | }, function(err, stdout, stderr) { 11 | assert.ifError(err); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/functional/03-evolve.provision.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var exec = require('child_process').exec; 5 | 6 | describe('cap production evolve:provision', function(done) { 7 | it('should not fail', function(done) { 8 | exec('bundle exec cap production evolve:provision', { 9 | cwd: process.cwd() + '/temp' 10 | }, function(err, stdout, stderr) { 11 | assert.ifError(err); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/functional/04-deploy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var exec = require('child_process').exec; 5 | 6 | describe('cap production deploy', function(done) { 7 | it('should not fail', function(done) { 8 | exec('bundle exec cap production deploy', { 9 | cwd: process.cwd() + '/temp' 10 | }, function(err, stdout, stderr) { 11 | assert.ifError(err); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/functional/05-evolve.db.up.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Browser = require('zombie'); 5 | var exec = require('child_process').exec; 6 | 7 | describe('cap production evolve:up:db', function(done) { 8 | it('should not fail', function(done) { 9 | exec('evolution_non_interactive=1 bundle exec cap production evolve:up:db', { 10 | cwd: process.cwd() + '/temp' 11 | }, function(err, stdout, stderr) { 12 | assert.ifError(err); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('should be installed', function(done) { 18 | var browser = new Browser(); 19 | 20 | browser 21 | .visit('http://example.com/') 22 | .then(null, function() { 23 | assert.equal('Evolution WordPress Test – Just another WordPress site', browser.text('title')); 24 | }) 25 | .then(done, done) 26 | ; 27 | }); 28 | 29 | it('should deny access to prod installer', function(done) { 30 | var browser = new Browser(); 31 | 32 | browser 33 | .visit('http://example.com/wp/wp-admin/install.php') 34 | .then(function() { 35 | assert(false, 'expected 403, got ' + browser.statusCode) 36 | }) 37 | .catch(function(error) { 38 | assert.equal(browser.statusCode, 403, "should be forbidden\n" + error); 39 | done() 40 | }) 41 | ; 42 | }); 43 | 44 | it('should have remote db backup', function(done) { 45 | exec('vagrant ssh local -c "ls -A /var/www/example.com/production/master/backups/example_db.*.gz"', { 46 | cwd: process.cwd() + '/temp' 47 | }, function(err, stdout, stderr) { 48 | assert.ifError(err); 49 | assert.notEqual(stdout, '') 50 | done(); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/functional/06-evolve.files.up.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Browser = require('zombie'); 5 | var fs = require('fs'); 6 | var exec = require('child_process').exec; 7 | 8 | var testFile = '/vagrant/web/wp-content/uploads/uploads_sync_test.jpg'; 9 | var testUrl = 'http://production.example.com/wp-content/uploads/uploads_sync_test.jpg'; 10 | 11 | describe('cap production evolve:files:up', function(done) { 12 | it('may need to remove uploads', function(done) { 13 | exec('vagrant ssh local -c "rm -f ' + testFile + '"', { 14 | cwd: process.cwd() + '/temp' 15 | }, function(err, stdout, stderr) { 16 | assert.ifError(err); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should have no uploads', function(done) { 22 | exec('evolution_non_interactive=1 bundle exec cap production evolve:files:up', { 23 | cwd: process.cwd() + '/temp' 24 | }, function(err, stdout, stderr) { 25 | assert.ifError(err); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should not exist at url', function(done) { 31 | var browser = new Browser(); 32 | 33 | browser 34 | .visit(testUrl) 35 | .then(function() { 36 | assert(false, "Url unexpectedly exists") 37 | }) 38 | .catch(function(error) { 39 | done(); 40 | }) 41 | ; 42 | }); 43 | 44 | it('may have to create upload', function(done) { 45 | exec('vagrant ssh local -c "touch ' + testFile + '"', { 46 | cwd: process.cwd() + '/temp' 47 | }, function(err, stdout, stderr) { 48 | assert.ifError(err); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('should sync uploads', function(done) { 54 | exec('evolution_non_interactive=1 bundle exec cap production evolve:files:up', { 55 | cwd: process.cwd() + '/temp' 56 | }, function(err, stdout, stderr) { 57 | assert.ifError(err); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should exist at url', function(done) { 63 | var browser = new Browser(); 64 | 65 | browser 66 | .visit(testUrl) 67 | .then(function() { 68 | done(); 69 | }) 70 | .catch(function(error) { 71 | assert(error) 72 | }) 73 | ; 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/functional/07-sni.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var exec = require('child_process').exec; 5 | var Browser = require('zombie'); 6 | 7 | describe('ssl server name indication', function(done) { 8 | it('local host should serve local cert', function(done) { 9 | exec('openssl s_client -connect local.example.com:443 -servername local.example.com', { 10 | cwd: process.cwd() + '/temp' 11 | }, function(err, stdout, stderr) { 12 | assert.ifError(! stdout.match('CN=local.example.com')); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('production.domain (1/3) should serve production cert', function(done) { 18 | exec('openssl s_client -connect production.example.com:443 -servername production.example.com', { 19 | cwd: process.cwd() + '/temp' 20 | }, function(err, stdout, stderr) { 21 | assert.ifError(! stdout.match('CN=example.com')); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('www.domain (2/3) should serve production cert', function(done) { 27 | exec('openssl s_client -connect www.example.com:443 -servername www.example.com', { 28 | cwd: process.cwd() + '/temp' 29 | }, function(err, stdout, stderr) { 30 | assert.ifError(! stdout.match('CN=example.com')); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('bare domain (3/3) should serve production cert', function(done) { 36 | exec('openssl s_client -connect production.example.com:443 -servername production.example.com', { 37 | cwd: process.cwd() + '/temp' 38 | }, function(err, stdout, stderr) { 39 | assert.ifError(! stdout.match('CN=example.com')); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('not vulnerable to CVE-2014-3566 (SSLv3 POODLE)', function(done) { 45 | exec('openssl s_client -connect local.example.com:443 -ssl3', { 46 | cwd: process.cwd() + '/temp' 47 | }, function(err, stdout, stderr) { 48 | assert.ifError(! stderr.match('routines:SSL3_READ_BYTES:sslv3 alert handshake failure')); 49 | done(); 50 | }); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /test/functional/08-robots.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Browser = require('zombie'); 5 | 6 | describe('robots.txt', function(done) { 7 | it('on local, should disallow all', function(done) { 8 | var browser = new Browser(); 9 | 10 | browser 11 | .visit('http://local.example.com/robots.txt') 12 | .then(function() { 13 | assert(!!browser.text().match('# Block everything...')); 14 | }) 15 | .then(done, done) 16 | ; 17 | }); 18 | 19 | it('on public prod, should point to sitemap', function(done) { 20 | var browser = new Browser(); 21 | 22 | browser 23 | .visit('http://example.com/robots.txt') 24 | .then(function() { 25 | assert(!!browser.text().match('# Sitemap: ')); 26 | }) 27 | .then(done, done) 28 | ; 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/functional/varnish/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Browser = require('zombie'); 5 | 6 | describe('Varnish', function() { 7 | it('should access backend', function(done) { 8 | var browser = new Browser(); 9 | 10 | browser 11 | .visit('http://example.com/') 12 | .then(null, function() { 13 | assert.equal('Evolution WordPress Test – Just another WordPress site', browser.text('title')); 14 | }) 15 | .then(done, done) 16 | ; 17 | }); 18 | 19 | describe('with no cookies', function() { 20 | it('should cache', function(done) { 21 | var browser = new Browser(); 22 | 23 | browser 24 | .visit('http://example.com/') 25 | .then(null, function() { 26 | assert.equal('cached', browser.resources[0].response.headers['x-cache']); 27 | }) 28 | .then(done, done) 29 | ; 30 | }); 31 | }); 32 | 33 | describe('with WordPress cookies', function() { 34 | it('should not cache', function(done) { 35 | var browser = new Browser(); 36 | 37 | browser 38 | .visit('http://example.com/wp-admin') 39 | .then(null, function() { 40 | assert(browser.resources.browser.getCookie('wordpress_test_cookie')); 41 | }) 42 | .then(null, function() { 43 | return browser.visit('http://example.com/'); 44 | }) 45 | .then(null, function() { 46 | assert.equal('Evolution WordPress Test – Just another WordPress site', browser.text('title')); 47 | assert.equal(0, browser.resources[0].response.headers.age); 48 | assert.equal('uncached', browser.resources[0].response.headers['x-cache']); 49 | }) 50 | .then(done, done) 51 | ; 52 | }); 53 | }); 54 | 55 | describe('with tracking cookies', function() { 56 | it('should ignore tracking cookies for cache', function(done) { 57 | var browser = new Browser(); 58 | 59 | browser.setCookie({ 60 | name: '_test', 61 | value: +new Date(), 62 | domain: 'example.com', 63 | path: '/', 64 | }); 65 | 66 | browser 67 | .visit('http://example.com/') 68 | .then(null, function() { 69 | assert(browser.getCookie('_test')); 70 | assert(browser.resources[0].response.headers.age); 71 | assert.equal('cached', browser.resources[0].response.headers['x-cache']); 72 | }) 73 | .then(done, done) 74 | ; 75 | }); 76 | }); 77 | 78 | describe('with an application cookies', function() { 79 | var cookie = { 80 | name: 'test', 81 | value: +new Date(), 82 | domain: 'example.com', 83 | path: '/', 84 | }; 85 | 86 | it('should not be cached initially', function(done) { 87 | var browser = new Browser(); 88 | 89 | browser.setCookie(cookie); 90 | 91 | browser 92 | .visit('http://example.com/') 93 | .then(null, function() { 94 | assert(browser.getCookie('test')); 95 | assert.equal(0, browser.resources[0].response.headers.age); 96 | assert.equal('uncached', browser.resources[0].response.headers['x-cache']); 97 | }) 98 | .then(done, done) 99 | ; 100 | }); 101 | 102 | it('should be subsequently cached', function(done) { 103 | var browser = new Browser(); 104 | 105 | browser.setCookie(cookie); 106 | 107 | browser 108 | .visit('http://example.com/') 109 | .then(null, function() { 110 | assert(browser.getCookie('test')); 111 | assert(browser.resources[0].response.headers.age); 112 | assert.equal('cached', browser.resources[0].response.headers['x-cache']); 113 | }) 114 | .then(done, done) 115 | ; 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/functional/wordpress/redirect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Browser = require('zombie'); 5 | 6 | describe('WordPress', function() { 7 | it('should redirect /wp-admin to /wp/wp-admin', function(done) { 8 | var browser = new Browser(); 9 | 10 | browser 11 | .visit('http://example.com/wp-admin') 12 | .then(function() { 13 | var location = browser.location.toString(); 14 | 15 | assert(browser.redirected); 16 | assert.equal(200, browser.statusCode); 17 | assert.equal(0, location.indexOf('http://example.com/wp/wp-login')); 18 | }) 19 | .then(done, done) 20 | ; 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/unit/wordpress/EvolutionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('test_local', Evolution::getDbName('test')); 15 | } 16 | 17 | /** 18 | * @dataProvider serverNameProvider 19 | */ 20 | public function testGetEnv($servername, $env, $hostname) 21 | { 22 | $_SERVER['SERVER_NAME'] = $servername; 23 | 24 | $this->assertEquals($env, Evolution::getEnv()); 25 | } 26 | 27 | /** 28 | * @dataProvider serverNameProvider 29 | */ 30 | public function testInitEnv($servername, $env, $hostname) 31 | { 32 | $_SERVER['SERVER_NAME'] = $servername; 33 | 34 | Evolution::initEnv(); 35 | 36 | $this->assertTrue(defined('WP_ENV'), '`Evolution::initEnv()` should define `WP_ENV`'); 37 | $this->assertEquals($env, WP_ENV); 38 | } 39 | 40 | /** 41 | * @dataProvider serverNameProvider 42 | */ 43 | public function testGetHostname($servername, $env, $hostname) 44 | { 45 | $_SERVER['SERVER_NAME'] = $servername; 46 | 47 | $this->assertEquals($hostname, Evolution::getHostname()); 48 | } 49 | 50 | public function serverNameProvider() 51 | { 52 | return array( 53 | array(Evolution::DOMAIN, 'production', EVOLUTION_WWW_MODE . Evolution::DOMAIN), 54 | array('www.'.Evolution::DOMAIN, 'production', EVOLUTION_WWW_MODE . Evolution::DOMAIN), 55 | array('local.'.Evolution::DOMAIN, 'local', 'local.'.Evolution::DOMAIN), 56 | array('staging.'.Evolution::DOMAIN, 'staging', 'staging.'.Evolution::DOMAIN), 57 | array('production.'.Evolution::DOMAIN, 'production', EVOLUTION_WWW_MODE . Evolution::DOMAIN), 58 | array('local.thewrongdomain.com', 'production', EVOLUTION_WWW_MODE . Evolution::DOMAIN) 59 | ); 60 | } 61 | } 62 | --------------------------------------------------------------------------------