├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── .vagrantplugins ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── Vagrantfile ├── acceptance ├── command │ ├── prompt_spec.rb │ ├── provision_spec.rb │ ├── push_spec.rb │ └── status_spec.rb └── support-skeletons │ ├── basic │ ├── Vagrantfile │ └── dummy.box │ ├── prompt │ ├── Vagrantfile │ └── dummy.box │ └── provision │ ├── Vagrantfile │ └── dummy.box ├── docs ├── config.md ├── deployment_tracker.md ├── environments.md ├── puppet.md └── strategy.md ├── dummy.box ├── lib ├── log4r │ └── outputter │ │ └── deployment_tracker_outputter.rb ├── vagrant-managed-servers │ ├── action.rb │ └── action │ │ ├── download_status.rb │ │ ├── init_deployment_tracker.rb │ │ ├── take_synced_folder_ownership.rb │ │ ├── track_deployment_end.rb │ │ ├── track_deployment_start.rb │ │ ├── track_server_deployment_end.rb │ │ ├── track_server_deployment_start.rb │ │ └── upload_status.rb ├── vagrant-orchestrate.rb └── vagrant-orchestrate │ ├── action │ ├── filtermanaged.rb │ └── setcredentials.rb │ ├── command │ ├── command_mixins.rb │ ├── init.rb │ ├── push.rb │ ├── root.rb │ └── status.rb │ ├── config.rb │ ├── plugin.rb │ ├── repo_status.rb │ └── version.rb ├── locales └── en.yml ├── spec ├── spec_helper.rb └── vagrant-orchestrate │ └── command │ ├── init_spec.rb │ └── root_spec.rb ├── templates ├── environment │ └── servers.json.erb ├── puppet │ ├── Puppetfile.erb │ ├── hiera.yaml.erb │ └── hiera │ │ └── common.yaml.erb └── vagrant │ ├── .vagrantplugins.erb │ ├── Vagrantfile.erb │ └── dummy.box ├── vagrant-orchestrate.gemspec └── vagrant-spec.config.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | 16 | .vagrant/ 17 | .DS_Store 18 | servers.json 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/LineLength: 2 | Max: 120 3 | 4 | Metrics/ClassLength: 5 | Max: 150 6 | 7 | Style/Documentation: 8 | Enabled: false 9 | 10 | Metrics/MethodLength: 11 | Max: 20 12 | 13 | StringLiterals: 14 | EnforcedStyle: double_quotes 15 | 16 | Style/FileName: 17 | Enabled: false 18 | 19 | Style/RegexpLiteral: 20 | Enabled: false 21 | 22 | Metrics/AbcSize: 23 | Max: 100 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | before_install: 4 | - gem uninstall bundler --all --executables 5 | - gem install bundler --version '< 1.7.0' 6 | rvm: 7 | - 2.0.0 8 | deploy: 9 | provider: rubygems 10 | gem: vagrant-orchestrate 11 | on: 12 | tags: true 13 | api_key: 14 | secure: qKr5F4kkezl1Vp+wfPX/8IO/7O6lqVqut6/Op1baALCWTtrSLyJpBND1fWaKVf01NZiJTmNXhCTHAEzZjbGkiqBpSqMdR2g8ISxEgj8hScw46RLmYBzH6RdL503O1j7HHyJNf2cUPQjjzLNriPowYhp92aut4z0bMCxZer7bd8o= 15 | -------------------------------------------------------------------------------- /.vagrantplugins: -------------------------------------------------------------------------------- 1 | required_plugins = {} 2 | # Example usage: 3 | # required_plugins["plugin-name"] = { version: "1.2.3", source: "https://rubygems.org" } 4 | required_plugins["vagrant-orchestrate"] = {} 5 | required_plugins["vagrant-managed-servers"] = {} 6 | 7 | needs_restart = false 8 | required_plugins.each do |plugin, options| 9 | version = options[:version] 10 | 11 | unless Vagrant.has_plugin?(plugin, version) 12 | command = "vagrant plugin install #{plugin}" 13 | command += " --plugin-version #{version}" if version 14 | command += " --plugin-source #{options[:source]}" if options[:source] 15 | system command 16 | needs_restart = true 17 | end 18 | end 19 | 20 | if needs_restart 21 | exec "vagrant #{ARGV.join' '}" 22 | end 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.8.0 (November 23rd, 2015) 2 | 3 | - Add an action as part of the push command that will change ownership of all synced folders to the configured user to avoid permission conflicts. 4 | - Added config.orchestrate.take_synced_file_ownership and documented in docs/config.md (default is true) 5 | - Should have no impact on Windows systems, since `chown` is filtered out by the winrm communicator filter 6 | 7 | 0.7.2 (September 25th, 2015) 8 | 9 | - Include windows guest directive on winrm template to correct issues with incorrect 10 | guest identification. 11 | 12 | 0.7.1 (August 13th, 2015) 13 | 14 | - Add configuration option for disable_commit_guard, which when set to true will silence 15 | the message about uncommitted files. Use at your own risk. 16 | 17 | 0.7.0 (July 15th, 2015) 18 | 19 | - Add support for tracking deployments with [deployment-tracker](https://github.com/Cimpress-MCP/deployment-tracker). 20 | Add configuration option `config.orchestrate.tracker_host = 'deploymenttracker.mydomain.com'` 21 | 22 | 0.6.5 (June 27th, 2015) 23 | 24 | - Split GIT_BRANCH environment variable on slash and take the last element, since 25 | that is how Jenkins is providing it. [#40](https://github.com/Cimpress-MCP/vagrant-orchestrate/issues/40) 26 | 27 | 0.6.4 (June 26th, 2015) 28 | 29 | - Add common ignores to the `.gitignore` file in the root of the repo unless 30 | `--no-git` is specified. Fixes [#28](https://github.com/Cimpress-MCP/vagrant-orchestrate/issues/28) 31 | 32 | 0.6.3 (May 26th, 2015) 33 | 34 | - Extract the `required_plugins` definition and installation logic from the 35 | `Vagrantfile` to a new `.vagrantplugins` file per https://github.com/mitchellh/vagrant/issues/4347 36 | - Change required_plugins from array to hash[plugin-name] = {options}. This allows specifying specific versions of plugins to be installed as well as alternate gem sources, which is useful for internally hosted gems. 37 | - Fall back to the Vagrant environment's `root_path` if the working directory is 38 | not a git repo. Fixes [#34](https://github.com/Cimpress-MCP/vagrant-orchestrate/issues/34) 39 | 40 | 0.6.2 (May 25th, 2015) 41 | 42 | - Change the implementation of the `RepoStatus.repo` method from relying on a 43 | remote named `origin` to using the state of the local file system. Thanks @rnowosielski 44 | for the bug report. 45 | 46 | 0.6.1 (May 23rd, 2015) 47 | 48 | - Change the credentials manager to add the `smb_username` and `smb_password` to 49 | the synced folders for a machine if the communicator is `winrm`. For Windows, 50 | this means only a single credential prompt for both machine authentication and 51 | SMB auth. 52 | 53 | 0.6.0 (May 15th, 2015) 54 | 55 | - Refactor the push command to compose middleware actions rather than performing 56 | a bunch of work in the command itself. This means that a push using the `parallel` 57 | strategy will truly be parallel per box, as opposed to the old implementation where 58 | the `up`, `provision`, `upload_stats`, and `destroy` phases would each happen in 59 | parallel, but the phases would be done in series. 60 | - Change the `vagrant orchestrate status` command so that it will run in parallel. 61 | - Add `vagrant-orchestrate` as a default required plugin. Someone will have to 62 | install it "by hand" to access the init functionality, but other users pulling 63 | down a repo with a committed Vagrantfile will not, making each repo more self-contained. 64 | 65 | 0.5.3 (May 13th, 2015) 66 | 67 | - Fix a bug where the VAGRANT_ORCHESTRATE_USERNAME and VAGRANT_ORCHESTRATE_PASSWORD 68 | environment variable overrides weren't being read properly. The bug was repro'd 69 | on a Windows environment. 70 | 71 | 0.5.2 (May 8th, 2015) 72 | 73 | - Add the `--no-provision` option to the `orchestrate push` command. Useful for 74 | first timers to gain confidence in using the tool or to be able to just reboot servers. 75 | - Move the `winrm.transport = :sspinegotiate` declaration from the config level 76 | to within the managed server section, allowing local Windows VMs to work again. 77 | - Change the default for --puppet-librarian-puppet to false, as it was impacting 78 | many Windows users. 79 | 80 | 0.5.1 (April 27th, 2015) 81 | 82 | - Also short circuit a push operation with an error message if there are untracked files. 83 | 84 | 0.5.0 (April 22, 2015) 85 | 86 | - Add guard_clean so that a push will fail if there are uncommitted files. Override with VAGRANT_ORCHESTRATE_NO_GUARD_CLEAN 87 | - Push a status file including git remote url, ref, user, and date on a successful provision to /var/status/vagrant_orchestrate (c:\programdata\vagrant_orchestrate) 88 | - Retrieve status using `vagrant orchestrate status` 89 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in vagrant-orchestrate.gemspec 4 | gemspec 5 | 6 | group :development do 7 | gem "vagrant", git: "https://github.com/mitchellh/vagrant.git", ref: "v1.7.2" 8 | gem "vagrant-spec", git: "https://github.com/mitchellh/vagrant-spec.git" 9 | end 10 | 11 | group :plugins do 12 | gem "vagrant-orchestrate", path: "." 13 | gem "vagrant-managed-servers" 14 | gem "vagrant-librarian-puppet" 15 | gem "vagrant-winrm-s" 16 | end 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Cimpress 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Cimpress-MCP/vagrant-orchestrate.svg?branch=master)](https://travis-ci.org/Cimpress-MCP/vagrant-orchestrate) 2 | [![Gem Version](https://badge.fury.io/rb/vagrant-orchestrate.svg)](http://badge.fury.io/rb/vagrant-orchestrate) 3 | 4 | # Vagrant Orchestrate 5 | 6 | ![](http://i.imgur.com/71yAw5v.gif) 7 | 8 | This is a Vagrant 1.6+ plugin that allows orchestrated deployments 9 | to already provisioned (non-elastic) servers on top of the excellent [Vagrant Managed Servers](http://github.com/tknerr/vagrant-managed-servers) plugin. 10 | It features a powerful templating `init` command, support for multiple environments, several deployment strategies 11 | and is designed from the ground up to be cross-platform, with first class support for **Windows, 12 | Linux, and Mac**. 13 | 14 | ## Quick start 15 | 16 | ``` 17 | $ vagrant orchestrate init --servers myserver1.mydomain.com,myserver2.mydomain.com \ 18 | --ssh-username USERNAME --ssh-private-key-path PATH \ 19 | --shell --shell-inline "echo Hello" 20 | 21 | $ ls 22 | Vagrantfile dummy.box 23 | $ vagrant orchestrate push 24 | ==> managed-myserver1.mydomain.com: Linking vagrant with managed server myserver1.mydomain.com 25 | ==> managed-myserver1.mydomain.com: -- Server: myserver1.mydomain.com 26 | ==> managed-myserver1.mydomain.com: Rsyncing folder: ~/dev/demo => /vagrant 27 | ==> managed-myserver1.mydomain.com: Running provisioner: shell... 28 | ==> managed-myserver1.mydomain.com: Running: inline script 29 | ==> managed-myserver1.mydomain.com: Hello 30 | ==> managed-myserver1.mydomain.com: Unlinking vagrant from managed server myserver1.mydomain.com 31 | ==> managed-myserver1.mydomain.com: -- Server: myserver1.mydomain.com 32 | ==> managed-myserver2.mydomain.com: Linking vagrant with managed server myserver2.mydomain.com 33 | ==> managed-myserver2.mydomain.com: -- Server: myserver2.mydomain.com 34 | ==> managed-myserver2.mydomain.com: Rsyncing folder: ~/dev/demo => /vagrant 35 | ==> managed-myserver2.mydomain.com: Running provisioner: shell... 36 | ==> managed-myserver2.mydomain.com: Running: inline script 37 | ==> managed-myserver2.mydomain.com: Hello 38 | ==> managed-myserver2.mydomain.com: Unlinking vagrant from managed server myserver2.mydomain.com 39 | ==> managed-myserver2.mydomain.com: -- Server: myserver2.mydomain.com 40 | ``` 41 | 42 | This also works for Windows with the `--winrm --winrm-username USERNAME --wirnm-password PASSWORD` parameters, but must be initiated from a Windows host. 43 | 44 | ## Usage 45 | 46 | Install using the standard Vagrant plugin installation method: 47 | 48 | $ vagrant plugin install vagrant-orchestrate 49 | 50 | ### Initialization 51 | Initialize a Vagrantfile to orchestrate running a script on multiple managed servers 52 | 53 | $ vagrant orchestrate init --shell 54 | 55 | Which produces a simple default Vagrantfile that can push to managed servers: 56 | ```ruby 57 | managed_servers = %w( ) 58 | 59 | Vagrant.configure("2") do |config| 60 | # This disables up, provision, reload, and destroy for managed servers. Use 61 | # `vagrant orchestrate push` to communicate with managed servers. 62 | config.orchestrate.filter_managed_commands = true 63 | 64 | config.vm.provision "shell", path: "{{YOUR_SCRIPT_PATH}}" 65 | config.ssh.username = "{{YOUR_SSH_USERNAME}}" 66 | config.ssh.private_key_path = "{{YOUR_SSH_PRIVATE_KEY_PATH}}" 67 | 68 | config.vm.define "local", primary: true do |local| 69 | local.vm.box = "ubuntu/trusty64" 70 | end 71 | 72 | managed_servers.each do |instance| 73 | config.vm.define instance, autostart: false do |box| 74 | box.vm.box = "managed-server-dummy" 75 | box.vm.box_url = "./dummy.box" 76 | box.vm.provider :managed do |provider| 77 | provider.server = instance 78 | end 79 | end 80 | end 81 | end 82 | ``` 83 | 84 | You'll need to edit your Vagrantfile and replace some variables, such as ssh username and 85 | private key, and the path to the script to run. Alternatively, you can pass them on the command 86 | line with `--ssh-username` and `--ssh-private-key-path`. The first line of the file defines an whitespace delimeted 87 | array of managed servers that the `push` command will operate on. 88 | 89 | ```ruby 90 | managed_servers = %w( myserver1.mydomain.com myserver2.mydomain.com ) 91 | ``` 92 | #### Windows 93 | 94 | This works for Windows managed servers using WinRM as well 95 | 96 | $ vagrant orchestrate init --winrm --winrm-username USERNAME --winrm-password PASSWORD 97 | 98 | ```ruby 99 | required_plugins = %w( vagrant-managed-servers vagrant-winrm-s ) 100 | 101 | ... 102 | 103 | config.vm.communicator = "winrm" 104 | config.winrm.username = "USERNAME" 105 | config.winrm.password = "PASSWORD" 106 | ``` 107 | 108 | #### Plugins 109 | 110 | This also supports a portable and repeatable way to install plugins, just list them in the .vagrantplugins file. 111 | `version` and `source` are the supported values in the options hash, but neither is required. 112 | 113 | ```ruby 114 | required_plugins = {} 115 | required_plugins["vagrant-orchestrate"] = {} 116 | required_plugins["vagrant-managed-servers"] = { version: "0.7.0" } 117 | ``` 118 | 119 | If you are executing in a shared environment, like a build slave, you can create your own 120 | own plugin install directory by setting the `VAGRANT_HOME` variable to something relative 121 | to the current directory. 122 | 123 | $ VAGRANT_HOME=./.vagrant.d vagrant orchestrate push 124 | 125 | or 126 | 127 | > SET VAGRANT_HOME=./.vagrant.d 128 | > vagrant orchestrate push 129 | 130 | #### Working with multiple environments 131 | 132 | Vagrant Orchestrate offers a way to manage multiple environments using a combination of a single servers.json file and the name of the current git branch as an indicator of the current environment. 133 | 134 | To initialize an environment aware Vagrantfile, use 135 | 136 | $ vagrant orchestrate init --environments dev,test,prod 137 | 138 | You'll need to create git branches with matching names and enter data into the the servers.json 139 | file in order for the Vagrantfile to be git branch aware. 140 | 141 | Learn more about [environments](docs/environments.md). 142 | 143 | #### Credentials 144 | 145 | Vagrant orchestrate offers the capability to prompt for credentials from the command 146 | line at the time of a push. You can initialize your Vagrantfile to declare this 147 | by passing the `--credentials-prompt` flag to the `vagrant orchestrate init` command, 148 | or add the following to your Vagrantfile. 149 | 150 | ```ruby 151 | config.orchestrate.credentials.prompt = true 152 | ``` 153 | 154 | The credentials config object can accept one additional parameter, `file_path`. Setting 155 | `creds.file_path = path/to/username_password.yaml` tells vagrant-orchestrate to 156 | look for a file at the given path, and read from its :username and :password fields 157 | ('username' and 'password' are also accepted). Additionally, you can pass the username 158 | and password in using the `VAGRANT_ORCHESTRATE_USERNAME` and `VAGRANT_ORCHESTRATE_PASSWORD` 159 | environment variables. Environment variables take precedence over the file, and the file 160 | takes precedence over the prompting. It is possible to set `prompt` to `false`, or leave 161 | it unset, in which case only environment variables and the credentials file (if provided) 162 | will be checked. 163 | 164 | #### Puppet 165 | 166 | Experimental [puppet templating](docs/puppet.md) support is available as well with the `--puppet` flag and associated options 167 | 168 | ### Configuraiton 169 | 170 | See details on the various configuration options [here](docs/config.md). 171 | 172 | ### Pushing changes 173 | Go ahead and push changes to your managed servers, in serial by default. 174 | 175 | $ vagrant orchestrate push 176 | 177 | The push command is currently limited to vagrant machines that use the `:managed` provider. So if you have other, local machines defined in the Vagrantfile, `vagrant orchestrate push` will not operate on those. 178 | 179 | Similar to the `up` and `provision` commands in vagrant, the `push` command can take in a regular expression for matching only certain machines. This is useful if your Vagrantfile contains all the managed machines, but you only want to push changes to a subset of those. 180 | 181 | ### Filtering managed commands 182 | It can be easy to make mistakes such as rebooting a production server if you have managed long-lived servers as well as local VMs defined in your Vagrantfile. We add some protection with the `orchestrate.filter_managed_commands` configuration setting, which will cause up, provision, reload, and destroy commands to be ignored for servers with the managed provider. This can be disabled by setting the variable to false in the Vagrantfile. 183 | 184 | ```ruby 185 | config.orchestrate.filter_managed_commands = true 186 | ``` 187 | 188 | #### Deployment Strategy 189 | 190 | Vagrant Orchestrate supports several deployment [strategies](docs/strategy.md) including parallel, canary, and half and half. 191 | 192 | You can push changes to all of your servers in parallel with 193 | 194 | $ vagrant orchestrate push --strategy parallel 195 | 196 | ### Status 197 | The `vagrant orchestrate status` command will reach out to each of the defined 198 | managed servers and print information about the last successful push from this 199 | repo, including date, ref, and user that performed the push. 200 | 201 | ``` 202 | $ vagrant orchestrate status 203 | Current managed server states: 204 | 205 | managed-1 2015-04-19 00:46:22 UTC e983dddd8041c5db77494266328f1d266430f57d cbaldauf 206 | managed-2 2015-04-19 00:46:22 UTC e983dddd8041c5db77494266328f1d266430f57d cbaldauf 207 | managed-3 Status unavailable. 208 | managed-4 2015-04-19 00:43:07 UTC e983dddd8041c5db77494266328f1d266430f57d cbaldauf 209 | ``` 210 | 211 | ### Tracking Deployment metadata 212 | 213 | Vagrant Orchestrate has support for integrating with [deployment-tracker](https://github.com/Cimpress-MCP/deployment-tracker), 214 | which can collect and record metadata about deployments, including summary records, metrics, and logs. This is great if you're 215 | in a distributed deployment environment, but need a central store for operational or compliance reasons. 216 | 217 | Add the following configuration option to your Vagrantfile 218 | 219 | config.orchestrate.tracker_host = "deploymenttracker.mydomain.com" 220 | 221 | See more info on [deployment tracker integration](docs/deployment_tracker.md). 222 | 223 | ## Ubuntu 224 | 225 | ### Sudoers 226 | 227 | * When deploying you must have no pasword prompt setup. You can do this by adding the following to your sudoers file. (edit via `sudo visudo`) 228 | 229 | `UserName ALL=(ALL:ALL) NOPASSWD:ALL` or `%GroupName ALL=(ALL:ALL) NOPASSWD:ALL` 230 | 231 | ## Windows 232 | 233 | ### Host 234 | 235 | * Need rsync? Install [OpenSSH](http://www.mls-software.com/opensshd.html) and then run this [script](https://github.com/joefitzgerald/packer-windows/blob/master/scripts/rsync.bat) to install rsync. Vagrant managed servers currently only works with cygwin based rsync implementations. 236 | * You MUST have at least PowerShell 3 installed in order to use the SMB folder synch. If you have PowerShell 237 | 2 installed and try to execute a folder sync, it will hang with no good error message. 238 | * Libcurl.dll must be installed and on your path. You can find it [here](https://github.com/danielcavanagh/typhoeus/blob/b8c089f1576cc5094eb84380c98a9146ab9dbba4/README.textile#windows-support) 239 | 240 | Alternative to the above instructions there is a [puppet module](https://github.com/gradifi/win_vagrant_orchestrate) that will setup a windows machine for you. 241 | 242 | ### Managed Guest 243 | You'll need to bootstrap the target machine. The following script should get you there. 244 | 245 | ``` 246 | winrm quickconfig -q 247 | winrm set winrm/config/service/auth @{Negotiate="true"} 248 | winrm set winrm/config/service @{AllowUnencrypted="false"} 249 | winrm set winrm/config/winrs @{MaxShellsPerUser="25"} 250 | winrm set winrm/config/winrs @{MaxConcurrentUsers="25"} 251 | sc config winrm start= auto 252 | sc config winrm type= own 253 | ``` 254 | * Check out [the winrm-s readme](https://github.com/Cimpress-MCP/vagrant-winrm-s/blob/master/README.md#setting-up-your-server) for more information 255 | 256 | ### Synced Folders 257 | 258 | See the [Synced Folders](https://github.com/tknerr/vagrant-managed-servers#synced-folders-windows) 259 | section of the Vagrant Managed Servers readme for more detail. 260 | 261 | ## Contributing 262 | 263 | 1. Fork it ( https://github.com/Cimpress-MCP/vagrant-orchestrate/fork ) 264 | 2. Create your feature branch (`git checkout -b my-new-feature`) 265 | 3. Commit your changes (`git commit -am 'Add some feature'`) 266 | 4. Run locally with `bundle exec vagrant orchestrate [init|push|status]` 267 | 5. `bundle exec rake build` 268 | 6. `bundle exec rake acceptance`, which will take a few minutes 269 | 7. Push to the branch (`git push origin my-new-feature`) 270 | 8. Create a new Pull Request 271 | 272 | Prerequisites: 273 | * Ruby 2.0 or greater 274 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | require "rubocop/rake_task" 4 | 5 | RuboCop::RakeTask.new 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task build: ["rubocop:auto_correct", :spec] 9 | task default: :build 10 | 11 | desc "Run acceptance tests with vagrant-spec" 12 | task :acceptance do 13 | puts "Bringing up target servers and syncing with NTP" 14 | # Spinning up local servers here, which the managed provider will connect to 15 | # by IP. See the Vagrantfile in the root of the repo for more info. 16 | system("vagrant up /local/ --no-provision") 17 | # To ensure the ntp sync happens even if the servers are already up 18 | system("vagrant provision /local/") 19 | ENV["VAGRANT_ORCHESTRATE_NO_GUARD_CLEAN"] = "true" 20 | system("bundle exec vagrant-spec test \ 21 | --components=orchestrate/push orchestrate/prompt orchestrate/status orchestrate/provision") 22 | puts "Destroying target servers" 23 | system("vagrant destroy -f /local/") 24 | end 25 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | managed_servers = %w( 192.168.10.80 192.168.10.81 192.168.10.82 192.168.10.83) 2 | 3 | Vagrant.configure(2) do |config| 4 | config.orchestrate.filter_managed_commands = true 5 | config.vm.provision :shell, inline: "echo hello" 6 | 7 | # These boxes are defined locally to enable acceptance testing. Spinning up 8 | # real boxes in the vagrant-spec environment was expensive because it ignored 9 | # the cache and didn't expose a facility to view the vagrant output as it ran. 10 | # These machines get spun up in the rake task and then the vagrant-spec tests 11 | # connect to them by IP address. 12 | managed_servers.each_with_index do |ip, index| 13 | config.vm.define "local-#{index + 1}" do |ubuntu| 14 | # minimize clock skew, since we're using the `date` command to measure 15 | # clock skew. 16 | ubuntu.vm.provision :shell, inline: "ntpdate pool.ntp.org" 17 | ubuntu.vm.box = "ubuntu/trusty64" 18 | ubuntu.vm.network "private_network", ip: ip 19 | end 20 | end 21 | 22 | # These managed boxes connect to the local boxes defined above by ip address. 23 | managed_servers.each_with_index do |server, index| 24 | config.vm.define "managed-#{index + 1}" do |managed| 25 | managed.vm.box = "managed-server-dummy" 26 | managed.vm.box_url = "./dummy.box" 27 | managed.ssh.password = "vagrant" 28 | managed.vm.provider :managed do |provider| 29 | provider.server = server 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /acceptance/command/prompt_spec.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-spec" 2 | 3 | describe "vagrant orchestrate prompt", component: "orchestrate/prompt" do 4 | include_context "acceptance" 5 | 6 | before do 7 | environment.skeleton("prompt") 8 | end 9 | 10 | # Vagrant throws with the error message below if a prompt is encountered. We need 11 | # to make sure that non-push commands don't prompt 12 | # Vagrant is attempting to interface with the UI in a way that requires 13 | # a TTY. Most actions in Vagrant that require a TTY have configuration 14 | # switches to disable this requirement. Please do that or run Vagrant 15 | # with TTY. 16 | it "doesn't prompt with non-push commands" do 17 | assert_execute("vagrant", "status") 18 | end 19 | 20 | # TODO: I wish there was a way to simulate prompting, but for now, that is left 21 | # to the user as a manual exercise. 22 | end 23 | -------------------------------------------------------------------------------- /acceptance/command/provision_spec.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-spec" 2 | 3 | describe "vagrant orchestrate provision", component: "orchestrate/provision" do 4 | include_context "acceptance" 5 | 6 | # This unique string gets echo'd as part of the provisioning process, so we 7 | # can check the output for this string. See ../support-skeletons/provision/Vagrantfile 8 | # for more info. 9 | PROVISION_STRING = "6etrabEmU8ru8hapheph" 10 | 11 | before do 12 | environment.skeleton("provision") 13 | end 14 | 15 | it "Runs the shell provisioner" do 16 | result = execute("vagrant", "orchestrate", "push", "managed-1") 17 | expect(result.stdout).to include(PROVISION_STRING) 18 | end 19 | 20 | it "Doesn't run with --no-provision" do 21 | result = execute("vagrant", "orchestrate", "push", "managed-1", "--no-provision") 22 | expect(result.stdout).not_to include(PROVISION_STRING) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /acceptance/command/push_spec.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-spec" 2 | 3 | describe "vagrant orchestrate push", component: "orchestrate/push" do 4 | include_context "acceptance" 5 | 6 | before do 7 | environment.skeleton("basic") 8 | end 9 | 10 | it "can push to a set of managed servers" do 11 | assert_execute("vagrant", "orchestrate", "push") 12 | end 13 | 14 | describe "strategies" do 15 | it "can push in parallel" do 16 | assert_execute("vagrant", "orchestrate", "push", "--strategy", "parallel") 17 | 18 | machine_names = (1..4).collect { |i| "managed-#{i}" } 19 | datetimes = get_sync_times(machine_names) 20 | execute("vagrant", "destroy", "-f") 21 | 22 | # Parallel provisioning should happen within a few seconds. 23 | ensure_datetimes_within(datetimes, 5) 24 | end 25 | 26 | it "can push with carary strategy" do 27 | assert_execute("vagrant", "orchestrate", "push", "--strategy", "canary", "-f") 28 | canary = get_sync_times(["managed-1"]).first 29 | the_rest = get_sync_times((2..4).collect { |i| "managed-#{i}" }) 30 | execute("vagrant", "destroy", "-f") 31 | ensure_datetimes_within(the_rest, 5) 32 | expect(diff_seconds(canary, the_rest.min)).to be >= 3 33 | end 34 | 35 | it "can push with half_half strategy" do 36 | assert_execute("vagrant", "orchestrate", "push", "--strategy", "half_half", "-f") 37 | first_half = get_sync_times(["managed-1", "managed-2"]) 38 | second_half = get_sync_times(["managed-3", "managed-4"]) 39 | execute("vagrant", "destroy", "-f") 40 | ensure_datetimes_within(first_half, 5) 41 | ensure_datetimes_within(second_half, 5) 42 | expect(diff_seconds(first_half.max, second_half.min)).to be >= 3 43 | end 44 | 45 | it "can push with carary_half_half strategy" do 46 | assert_execute("vagrant", "orchestrate", "push", "--strategy", "canary_half_half", "-f") 47 | canary = get_sync_times(["managed-1"]).first 48 | first_half = get_sync_times(["managed-2"]) 49 | second_half = get_sync_times(["managed-3", "managed-4"]) 50 | execute("vagrant", "destroy", "-f") 51 | ensure_datetimes_within(first_half, 5) 52 | expect(diff_seconds(canary, first_half.min)).to be > 3 53 | expect(diff_seconds(first_half.max, second_half.min)).to be >= 3 54 | end 55 | end 56 | 57 | def get_sync_times(machines) 58 | datetimes = [] 59 | machines.each do |machine| 60 | execute("vagrant", "up", machine) 61 | # This file is written by the shell provisioner in ../support-skeletons/basic/Vagrantfile 62 | result = execute("vagrant", "ssh", "-c", "cat /tmp/sync_time", machine) 63 | datetimes << DateTime.parse(result.stdout.chomp) 64 | end 65 | datetimes 66 | end 67 | 68 | # Ensure that the range (max - min) of the datetime objects passed in are within 69 | # the given number of seconds. 70 | def ensure_datetimes_within(datetimes, seconds) 71 | expect(diff_seconds(datetimes.min, datetimes.max)).to be < seconds 72 | end 73 | 74 | # The difference between two datetimes in seconds 75 | def diff_seconds(start, finish) 76 | ((finish - start) * 86_400).to_i 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /acceptance/command/status_spec.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-spec" 2 | require "vagrant-orchestrate/repo_status" 3 | 4 | describe "vagrant orchestrate status", component: "orchestrate/status" do 5 | include_context "acceptance" 6 | 7 | TEST_REF = "050bfd9c686b06c292a9614662b0ab1bbf652db3" 8 | TEST_REMOTE_ORIGIN_URL = "http://github.com/Cimpress-MCP/vagrant-orchestrate.git" 9 | TEST_REPO = "vagrant-orchestrate" 10 | 11 | before do 12 | environment.skeleton("basic") 13 | end 14 | 15 | it "handles no status file gracefully" do 16 | # Make sure we're starting from a clean slate, rspec order isn't guaranteed. 17 | execute("vagrant", "up", "managed-1") 18 | execute("vagrant", "ssh", "-c", "\"sudo rm -rf /var/state/vagrant_orchestrate\"", "managed-1") 19 | # All commands are executed against a single machine to reduce variability 20 | result = execute("vagrant", "orchestrate", "status", "/managed-1/") 21 | expect(result.stdout).to include("Status unavailable.") 22 | end 23 | 24 | it "can push and retrieve status" do 25 | # Because vagrant-spec executes in a clean tmp folder, it isn't a git repo, 26 | # and the normal git commands don't work. We'll inject some test data using 27 | # environment variables. See vagrant-orchestrate/repo_status.rb for impl. 28 | ENV["VAGRANT_ORCHESTRATE_STATUS_TEST_REF"] = TEST_REF 29 | ENV["VAGRANT_ORCHESTRATE_STATUS_TEST_REMOTE_ORIGIN_URL"] = TEST_REMOTE_ORIGIN_URL 30 | ENV["VAGRANT_ORCHESTRATE_STATUS_TEST_REPO"] = TEST_REPO 31 | ENV["VAGRANT_ORCHESTRATE_NO_GUARD_CLEAN"] = "true" 32 | execute("vagrant", "orchestrate", "push", "/managed-1/") 33 | result = execute("vagrant", "orchestrate", "status", "/managed-1/") 34 | status = VagrantPlugins::Orchestrate::RepoStatus.new(TEST_REPO) 35 | # Punting on date. Can always add it later if needed 36 | expect(result.stdout).to include(status.ref) 37 | expect(result.stdout).to include(status.user) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /acceptance/support-skeletons/basic/Vagrantfile: -------------------------------------------------------------------------------- 1 | managed_servers = %w( 192.168.10.80 192.168.10.81 192.168.10.82 192.168.10.83) 2 | 3 | Vagrant.configure(2) do |config| 4 | config.ssh.password = "vagrant" 5 | managed_servers.each_with_index do |server, index| 6 | config.vm.define "managed-#{index + 1}" do |managed| 7 | managed.vm.provision "shell", inline: "date > /tmp/sync_time; sleep 3" 8 | managed.vm.box = "managed-server-dummy" 9 | managed.vm.box_url = "./dummy.box" 10 | managed.ssh.password = "vagrant" 11 | managed.vm.provider :managed do |provider| 12 | provider.server = server 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /acceptance/support-skeletons/basic/dummy.box: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimpress-MCP/vagrant-orchestrate/16487bbcbc49846b4b23d1a542c2231f1e96d194/acceptance/support-skeletons/basic/dummy.box -------------------------------------------------------------------------------- /acceptance/support-skeletons/prompt/Vagrantfile: -------------------------------------------------------------------------------- 1 | managed_servers = %w( 192.168.10.80 192.168.10.81 192.168.10.82 192.168.10.83 ) 2 | 3 | Vagrant.configure(2) do |config| 4 | config.orchestrate.credentials.prompt = true 5 | 6 | managed_servers.each_with_index do |server, index| 7 | config.vm.define "managed-#{index + 1}" do |managed| 8 | managed.vm.provision "shell", inline: "echo 'hello world'" 9 | managed.vm.box = "managed-server-dummy" 10 | managed.vm.box_url = "./dummy.box" 11 | managed.vm.provider :managed do |provider| 12 | provider.server = server 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /acceptance/support-skeletons/prompt/dummy.box: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimpress-MCP/vagrant-orchestrate/16487bbcbc49846b4b23d1a542c2231f1e96d194/acceptance/support-skeletons/prompt/dummy.box -------------------------------------------------------------------------------- /acceptance/support-skeletons/provision/Vagrantfile: -------------------------------------------------------------------------------- 1 | managed_servers = %w( 192.168.10.80 192.168.10.81 192.168.10.82 192.168.10.83 ) 2 | 3 | Vagrant.configure(2) do |config| 4 | # Print a unique string that we can check the output for 5 | config.vm.provision :shell, inline: "echo 6etrabEmU8ru8hapheph" 6 | 7 | managed_servers.each_with_index do |server, index| 8 | config.vm.define "managed-#{index + 1}" do |managed| 9 | managed.vm.provision "shell", inline: "echo 'hello world'" 10 | managed.vm.box = "managed-server-dummy" 11 | managed.vm.box_url = "./dummy.box" 12 | managed.ssh.password = "vagrant" 13 | managed.vm.provider :managed do |provider| 14 | provider.server = server 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /acceptance/support-skeletons/provision/dummy.box: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimpress-MCP/vagrant-orchestrate/16487bbcbc49846b4b23d1a542c2231f1e96d194/acceptance/support-skeletons/provision/dummy.box -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Configuration Options 2 | 3 | All configuration options start with `config.orchestrate` 4 | 5 | ## `disable_commit_guard` 6 | 7 | By default, Vagrant Orchestrate has protection to disallow a push action to managed 8 | servers if there are any uncommitted or untracked files in your git repository. Setting 9 | the `disable_commit_guard` configuration option will disable this protection. 10 | 11 | config.orchestrate.disable_commit_guard = true 12 | 13 | ## `take_synced_folder_ownership` 14 | 15 | When multiple users are using Vagrant Orchestrate to push to the same target servers, 16 | there can arise permission issues for folders that are synced. When `true`, Vagrant 17 | Orchestrate will change ownership of the guestpath of all synced folders to be the 18 | `owner` specified in the `synced_folder` or the `ssh_info.username`. Has no impact 19 | for Windows guests. Default is `true`. 20 | -------------------------------------------------------------------------------- /docs/deployment_tracker.md: -------------------------------------------------------------------------------- 1 | # Deployment Tracker Integration 2 | 3 | [Deployment tracker](http://github.com/Cimpress-MCP/deployment-tracker) is a lightweight node REST service that can aggregate 4 | deployment metadata across your organization. You can record deployment execution times and outcomes using Vagrant Orchestrate 5 | whether you're deploying from your laptop to a cloud instance or from a build slave to a hardware box. 6 | 7 | ## What is actually tracked? 8 | 9 | 1. Deployments starting 10 | 2. Deployment starting on a server 11 | 3. Deployment completing on a server 12 | 4. Vagrant console output 13 | 5. Deployment completion 14 | 15 | The summary record for a successful deployment to 4 servers using the half_half strategy is recorded as follows: 16 | 17 | ``` 18 | curl http://deploymenttracker.mydomain.com/v1/deployments/120c7b1c-73bc-4626-bef8-be1983785562 19 | { 20 | "deployment_id": "120c7b1c-73bc-4626-bef8-be1983785562", 21 | "engine": "vagrant_orchestrate", 22 | "engine_version": "0.7.0", 23 | "host": "mypc.local", 24 | "user": "cbaldauf", 25 | "environment": "deployment_tracker", 26 | "package": "vagrant-orchestrate", 27 | "package_url": "https://github.com/Cimpress-MCP/vagrant-orchestrate.git", 28 | "version": "c3ede86e0a0b1a0c3cf6952906a914fdb3832f87", 29 | "arguments": "--strategy half_half", 30 | "createdAt": "2015-07-07T17:34:12.008Z", 31 | "updatedAt": "2015-07-07T17:34:12.008Z", 32 | "servers": [ 33 | { 34 | "hostname": "192.168.10.81", 35 | "deployment_id": "120c7b1c-73bc-4626-bef8-be1983785562", 36 | "result": "success", 37 | "elapsed_seconds": 3, 38 | "createdAt": "2015-07-07 17:34:12.075 +00:00", 39 | "updatedAt": "2015-07-07 17:34:15.730 +00:00" 40 | }, 41 | { 42 | "hostname": "192.168.10.80", 43 | "deployment_id": "120c7b1c-73bc-4626-bef8-be1983785562", 44 | "result": "success", 45 | "elapsed_seconds": 3, 46 | "createdAt": "2015-07-07 17:34:12.281 +00:00", 47 | "updatedAt": "2015-07-07 17:34:16.044 +00:00" 48 | }, 49 | { 50 | "hostname": "192.168.10.83", 51 | "deployment_id": "120c7b1c-73bc-4626-bef8-be1983785562", 52 | "result": "success", 53 | "elapsed_seconds": 3, 54 | "createdAt": "2015-07-07 17:34:26.113 +00:00", 55 | "updatedAt": "2015-07-07 17:34:29.477 +00:00" 56 | }, 57 | { 58 | "hostname": "192.168.10.82", 59 | "deployment_id": "120c7b1c-73bc-4626-bef8-be1983785562", 60 | "result": "success", 61 | "elapsed_seconds": 20, 62 | "createdAt": "2015-07-07 17:34:26.316 +00:00", 63 | "updatedAt": "2015-07-07 17:34:46.475 +00:00" 64 | } 65 | ] 66 | } 67 | 68 | ``` 69 | 70 | ### Field Values 71 | 72 | If you are using the Vagrant Orchestrate git integration, which is highly recommended, the following 73 | field values will be assigned automatically: 74 | 75 | 1. deployment_id - UUID generated by Vagrant Orchestrate 76 | 2. environment - git branch name 77 | 3. package - git repo name 78 | 4. package_url - git remote origin url 79 | 5. version - git SHA 80 | 81 | 82 | ## Configuration 83 | 84 | Add the following configuration option to your Vagrantfile 85 | 86 | config.orchestrate.tracker_host = "http://deploymenttracker.mydomain.com" 87 | 88 | ## Logging 89 | 90 | Each line sent to Vagrant's UI (your console) will be sent to deployment tracker and 91 | forwarded on to an appropriate log collector. The current implementation is naive and 92 | creates an HTTP POST request for each line printed to the console. If this is causing 93 | performance issues with your deployment, you can disable it with: 94 | 95 | config.orchestrate.tracker_logging_enabled = false 96 | 97 | ## Initialization 98 | 99 | vagrant orchestrate init --deployment-tracker-host http://deploymenttracker.mydomain.com 100 | -------------------------------------------------------------------------------- /docs/environments.md: -------------------------------------------------------------------------------- 1 | # Environments 2 | It is a very common pattern in software development to have separate environments - e.g. dev, test, and prod. 3 | Vagrant Orchestrate offers a way to manage multiple environments using a combination of a single servers.json 4 | file and the name of the current git branch to know which the current environment is. 5 | 6 | ```javascript 7 | # servers.json 8 | { 9 | "environments": { 10 | "dev": { 11 | "servers": [ 12 | "dev.myapp.mydomain.com" 13 | ] 14 | }, 15 | "test": { 16 | "servers": [ 17 | "test1.myapp.mydomain.com", 18 | "test2.myapp.mydomain.com" 19 | ] 20 | }, 21 | "prod": { 22 | "servers": [ 23 | "prod1.myapp.mydomain.com", 24 | "prod2.myapp.mydomain.com", 25 | "prod3.myapp.mydomain.com" 26 | ] 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | Add the following line to the top of your `Vagrantfile` 33 | 34 | ```ruby 35 | managed_servers = VagrantPlugins::Orchestrate::Plugin.load_servers_for_branch 36 | ``` 37 | 38 | If you create git branches named `dev`, `test`, and `prod`, your vagrantfile will become environment aware and 39 | you'll only be able to see the servers appropriate for that environment. 40 | 41 | ``` 42 | $ git branch 43 | * dev 44 | test 45 | prod 46 | $ vagrant status 47 | Current machine states: 48 | 49 | local not created (virtualbox) 50 | dev.myapp.mydomain.com not created (managed) 51 | 52 | $ git checkout test 53 | Switched to branch 'test' 54 | $ vagrant status 55 | Current machine states: 56 | 57 | local not created (virtualbox) 58 | test1.myapp.mydomain.com not created (managed) 59 | test2.myapp.mydomain.com not created (managed) 60 | 61 | $ git checkout prod 62 | Switched to branch 'prod' 63 | $ vagrant status 64 | Current machine states: 65 | 66 | local not created (virtualbox) 67 | prod1.myapp.mydomain.com not created (managed) 68 | prod2.myapp.mydomain.com not created (managed) 69 | prod3.myapp.mydomain.com not created (managed) 70 | ``` 71 | 72 | Any branch that doesn't have a matching environment in the servers.json file will 73 | not list any managed servers. 74 | 75 | ``` 76 | $ git checkout -b my_feature_branch 77 | Switched to a new branch 'my_feature_branch' 78 | $ vagrant status 79 | Current machine states: 80 | 81 | local not created (virtualbox) 82 | ``` 83 | 84 | ## Init 85 | 86 | Vagrant Orchestrate will template the Vagrantfile and servers.json using `vagrant orchestrate init --environments dev,test,prod`. You will need to specify the servers that exist in each environment in the servers.json file and also create each of the appropriate branches. 87 | 88 | ## Merge conflicts 89 | 90 | Early in the development process, we experimented with a different servers.json file per branch, meaning that the servers.json in the dev branch would only list servers that existed in dev. This created many merge conflicts and this friction quickly led to frustration. In the end, we asserted that the servers.json file should be the same across environments in order to remove the merge conflicts from the equation. This means there is a higher cost to adding a new server because it needs to be added to all environment branches, but in my experience, servers change less often than code. 91 | -------------------------------------------------------------------------------- /docs/puppet.md: -------------------------------------------------------------------------------- 1 | # Puppet 2 | 3 | Experimental puppet templating support is available with the `--puppet` flag and associated options 4 | 5 | ```ruby 6 | required_plugins = %w( vagrant-managed-servers vagrant-librarian-puppet ) 7 | 8 | ... 9 | 10 | config.librarian_puppet.placeholder_filename = ".gitignore" 11 | config.vm.provision "puppet" do |puppet| 12 | puppet.module_path = 'modules' 13 | puppet.hiera_config_path = 'hiera.yaml' 14 | end 15 | ``` 16 | 17 | The following files and folders will be placed in the puppet directory 18 | 19 | ``` 20 | Puppetfile 21 | hieradata/ 22 | common.yaml 23 | hiera.yaml 24 | manifests/ 25 | default.pp 26 | modules/ 27 | .gitignore 28 | ``` 29 | 30 | For a full list of init options, run `vagrant orchestrate init --help` 31 | -------------------------------------------------------------------------------- /docs/strategy.md: -------------------------------------------------------------------------------- 1 | # Deployment Strategies 2 | 3 | Vagrant Orchestrate supports several deployment strategies that allow you to orchestrate the behavior pushes to remote servers. Here we'll cover how to use the various strategies as well as describing 4 | situations when each might be useful. 5 | 6 | ## Strategies 7 | 8 | ### Serial (default) 9 | Deploy to the target servers one at a time. This can be useful if you 10 | have a small number servers, or if you need to keep the majority of your servers 11 | online in order to support your application's load. 12 | 13 | $ vagrant orchestrate push --strategy serial 14 | 15 | config.orchestrate.strategy = :serial 16 | 17 | 18 | ### Parallel 19 | Deploy to all of the target servers at the same time. This is 20 | useful if you want to minimize the total amount of time that an deployment takes. 21 | Depending on how you've written your provisioners, this could cause downtime for 22 | the application that is being deployed. 23 | 24 | $ vagrant orchestrate push --strategy parallel 25 | 26 | config.orchestrate.strategy = :parallel 27 | 28 | ### Canary 29 | Deploy to a single server, pause to allow for testing, and then deploy the remainder of the servers in parallel. 30 | This is a great opportunity to test one node of your cluster before blasting your 31 | changes out to them all. This can be particularly useful when combined with post 32 | provision [trigger](https://github.com/emyl/vagrant-triggers) to run a smoke test. 33 | 34 | $ vagrant orchestrate push --strategy canary 35 | 36 | config.orchestrate.strategy = :canary 37 | 38 | The prompt can be surpressed with the `--force` (`-f`) flag. 39 | 40 | ### Half and Half 41 | Deploys to half of the cluster in parallel, then the other half, with 42 | a pause in between. This won't manage any of your load balancing or networking 43 | configuration for you, but if your application has a healthcheck that your load 44 | balancer respects, it should be easy to turn it off at the start of your provisioning 45 | and back on at the end. If your application can serve the load on half of its nodes 46 | then this will be the best blend of getting the deployment done quickly and maintaining 47 | a running application. If the total number of target servers is odd then the smaller 48 | number will be deployed to first. 49 | 50 | $ vagrant orchestrate push --strategy half_half 51 | 52 | config.orchestrate.strategy = :half_half 53 | 54 | ### Canary Half and Half 55 | Combines the two immediately above - deploying to a single 56 | server, pausing, then to half of the remaining cluster in parallel, pausing, and then the other half, 57 | also in parallel. This is good if you have a large number of servers and want to do a 58 | smoke test of a single server before committing to pushing to half of your farm. 59 | 60 | $ vagrant orchestrate push --strategy canary_half_half 61 | 62 | config.orchestrate.strategy = :canary_half_half 63 | 64 | ## Specifying a strategy 65 | 66 | ### Command line 67 | 68 | Strategies can be passed on the command line with the `--strategy` parameter 69 | 70 | $ vagrant orchestrate push --strategy parallel 71 | 72 | ### Vagrantfile configuration 73 | 74 | Alternatively, you can specify the deployment strategy in your Vagrantfile 75 | 76 | config.orchestrate.strategy = :parallel 77 | 78 | Command line parameters take precedence over configuration values set in the Vagrantfile. 79 | 80 | ## Suppressing Prompts 81 | In order to automate the deployment process, you'll need to suppress 82 | prompts. You can achieve that in two ways: 83 | 84 | From the command line, add the `--force` or `-f` parameters 85 | 86 | $ vagrant orchestrate push --strategy canary -f 87 | 88 | 89 | Within your Vagrantfile, set the `force_push` setting to true 90 | 91 | config.orchestrate.force_push = true 92 | 93 | ## Support for other strategies 94 | If you have ideas for other strategies that you think would be broadly useful, 95 | open an issue and we'll discuss. 96 | -------------------------------------------------------------------------------- /dummy.box: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimpress-MCP/vagrant-orchestrate/16487bbcbc49846b4b23d1a542c2231f1e96d194/dummy.box -------------------------------------------------------------------------------- /lib/log4r/outputter/deployment_tracker_outputter.rb: -------------------------------------------------------------------------------- 1 | require "log4r/outputter/outputter" 2 | require "time" 3 | 4 | module Log4r 5 | class DeploymentTrackerOutputter < Outputter 6 | def initialize(name, hash = {}) 7 | super(name, hash) 8 | @logger = Log4r::Logger.new("vagrant_orchestrate::log4r::deployment_tracker_outputter") 9 | end 10 | 11 | private 12 | 13 | def canonical_log(event) 14 | data = {} 15 | data["type"] = event.fullname 16 | data["timestamp"] = Time.now.getutc.iso8601 17 | data["level"] = LNAMES[event.level] 18 | data["message"] = event.data 19 | 20 | begin 21 | id = VagrantPlugins::Orchestrate::DEPLOYMENT_ID 22 | DeploymentTrackerClient::DefaultApi.post_logs(id, [data]) 23 | rescue 24 | @logger.warn "Unable to send log messages to deployment-tracker" 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-managed-servers/action/upload_status" 2 | require "vagrant-managed-servers/action/init_deployment_tracker" 3 | require "vagrant-managed-servers/action/take_synced_folder_ownership" 4 | require "vagrant-managed-servers/action/track_deployment_start" 5 | require "vagrant-managed-servers/action/track_deployment_end" 6 | require "vagrant-managed-servers/action/track_server_deployment_start" 7 | require "vagrant-managed-servers/action/track_server_deployment_end" 8 | 9 | # It is useful to be able to call up, provision, reload, and destroy as a single 10 | # unit - it makes things like parallel provisioning more seamless and provides 11 | # a useful action hook for the push command. 12 | module VagrantPlugins 13 | module ManagedServers 14 | module Action 15 | include Vagrant::Action::Builtin 16 | 17 | def self.action_push 18 | Vagrant::Action::Builder.new.tap do |b| 19 | b.use TrackServerDeploymentStart 20 | b.use action_up 21 | b.use TakeSyncedFolderOwnership 22 | b.use Call, action_provision do |env, b2| 23 | if env[:reboot] 24 | b2.use Call, action_reload do |_env, _b3| 25 | end 26 | end 27 | end 28 | b.use UploadStatus 29 | b.use action_destroy 30 | b.use TrackServerDeploymentEnd 31 | end 32 | end 33 | 34 | def self.action_download_status 35 | Vagrant::Action::Builder.new.tap do |b| 36 | b.use ConfigValidate 37 | b.use DownloadStatus 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/download_status.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ManagedServers 5 | module Action 6 | class DownloadStatus 7 | def initialize(app, _env) 8 | @app = app 9 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::print_status") 10 | end 11 | 12 | def call(env) 13 | download_status(env[:machine], env[:local_file_path], env[:remote_file_path], env[:ui]) 14 | 15 | @app.call(env) 16 | end 17 | 18 | def download_status(machine, local, remote, ui) 19 | machine.communicate.wait_for_ready(5) 20 | @logger.debug("Downloading orchestrate status for #{machine.name}") 21 | ui.info("Downloading orchestrate status from #{remote}") 22 | @logger.debug(" remote file: #{remote}") 23 | @logger.debug(" local file: #{local}") 24 | machine.communicate.download(remote, local) 25 | content = File.read(local) 26 | @logger.debug("File content:") 27 | @logger.debug(content) 28 | status = JSON.parse(content) 29 | ENV["VAGRANT_ORCHESTRATE_STATUS"] += machine.name.to_s + " " + status["last_sync"] + \ 30 | " " + status["ref"] + " " + status["user"] + "\n" 31 | rescue => ex 32 | ui.warn("Error downloading status for #{machine.name}.") 33 | ui.warn(ex.message) 34 | ENV["VAGRANT_ORCHESTRATE_STATUS"] += machine.name.to_s + " Status unavailable.\n" 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/init_deployment_tracker.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | require "log4r/outputter/deployment_tracker_outputter" 3 | 4 | module Vagrant 5 | module UI 6 | class Interface 7 | attr_reader :logger 8 | end 9 | end 10 | end 11 | 12 | module VagrantPlugins 13 | module ManagedServers 14 | module Action 15 | class InitDeploymentTracker 16 | def initialize(app, _env) 17 | @app = app 18 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::init_deployment_tracker") 19 | end 20 | 21 | def call(env) 22 | host = env[:tracker_host] 23 | return unless host 24 | SwaggerClient::Swagger.configure do |config| 25 | config.host = host 26 | end 27 | 28 | if env[:tracker_logging_enabled] 29 | ui = env[:ui] 30 | unless ui.logger.outputters.collect(&:name).include?("deployment-tracker") 31 | # Make sure that we've hooked the global ui logger as well. We should 32 | # see if we can do this earlier in the process to capture more of the output 33 | ui.logger.add Log4r::DeploymentTrackerOutputter.new("deployment-tracker") 34 | end 35 | end 36 | @app.call(env) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/take_synced_folder_ownership.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ManagedServers 5 | module Action 6 | class TakeSyncedFolderOwnership 7 | def initialize(app, _env) 8 | @app = app 9 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::take_synced_folder_ownership") 10 | end 11 | 12 | def call(env) 13 | take_synced_folder_ownership env[:machine], env[:ui] 14 | @app.call(env) 15 | end 16 | 17 | def take_synced_folder_ownership(machine, ui) 18 | return unless machine.config.orchestrate.take_synced_folder_ownership 19 | ui.info "Taking ownership of all guest synced folders" 20 | 21 | @logger.debug "Taking ownership of synced folders" 22 | machine.config.vm.synced_folders.each do |synced_folder| 23 | options = synced_folder[1] 24 | next if options[:disabled] 25 | options[:owner] ||= machine.ssh_info[:username] 26 | chown machine, options[:guestpath], options[:owner] 27 | end 28 | 29 | @logger.debug "Taking ownership of provisioner assets" 30 | machine.config.vm.provisioners.each do |provisioner| 31 | owner = machine.ssh_info[:username] 32 | chown machine, provisioner.config.upload_path, owner if provisioner.type == :shell 33 | chown machine, provisioner.config.temp_dir, owner if provisioner.type == :puppet 34 | end 35 | end 36 | 37 | def chown(machine, path, owner) 38 | @logger.debug "Taking ownership of #{path}" 39 | machine.communicate.sudo "chown '#{owner}' -R #{path}" 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/track_deployment_end.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ManagedServers 5 | module Action 6 | class TrackDeploymentEnd 7 | def initialize(app, _env) 8 | @app = app 9 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::track_deployment_end") 10 | end 11 | 12 | def call(env) 13 | track_deployment_end(env[:tracker_host], env[:start_time], env[:success], env[:ui]) 14 | @app.call(env) 15 | end 16 | 17 | def track_deployment_end(host, start, success, ui) 18 | return unless host 19 | @logger.debug("Tracking deployment end to #{host}.") 20 | id = VagrantPlugins::Orchestrate::DEPLOYMENT_ID 21 | ui.info("Deployment tracked in deployment-tracker with ID: #{id}") 22 | result = success ? "success" : "failure" 23 | elapsed_seconds = (Time.now - start).to_i 24 | deployment = { deployment_id: id, 25 | result: result, 26 | assert_empty_server_result: true, 27 | elapsed_seconds: elapsed_seconds } 28 | DeploymentTrackerClient::DefaultApi.put_deployment(id, deployment) 29 | rescue => ex 30 | ui.warn("There was an error notifying deployment tracker. See error log for details.") 31 | @logger.warn("Error tracking deployment end for deployment #{id}") 32 | @logger.warn(ex) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/track_deployment_start.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | require "vagrant-orchestrate/version" 3 | 4 | module VagrantPlugins 5 | module ManagedServers 6 | module Action 7 | class TrackDeploymentStart 8 | def initialize(app, _env) 9 | @app = app 10 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::track_deployment_start") 11 | end 12 | 13 | def call(env) 14 | track_deployment_start(env[:tracker_host], env[:status], env[:ui], env[:args]) 15 | @app.call(env) 16 | end 17 | 18 | def track_deployment_start(host, status, ui, args) 19 | return unless host 20 | id = VagrantPlugins::Orchestrate::DEPLOYMENT_ID 21 | ui.info("Deployment tracked in deployment-tracker with ID: #{id}") 22 | @logger.debug("Tracking deployment start to #{host}.") 23 | hostname = `hostname`.chomp 24 | deployment = { 25 | deployment_id: id, 26 | engine: "vagrant_orchestrate", 27 | engine_version: VagrantPlugins::Orchestrate::VERSION, 28 | user: status.user, host: hostname, 29 | environment: status.branch, 30 | package: status.repo, 31 | package_url: status.remote_origin_url, 32 | version: status.ref, arguments: args 33 | } 34 | DeploymentTrackerClient::DefaultApi.post_deployment(id, deployment) 35 | rescue => ex 36 | ui.warn("There was an error notifying deployment tracker. See error log for details.") 37 | @logger.warn("Error tracking deployment start for deployment #{id}") 38 | @logger.warn(ex) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/track_server_deployment_end.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ManagedServers 5 | module Action 6 | class TrackServerDeploymentEnd 7 | def initialize(app, _env) 8 | @app = app 9 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::track_server_deployment_end") 10 | end 11 | 12 | def call(env) 13 | machine = env[:machine] 14 | track_deployment_end(machine, env[:ui], env[:start_times][machine.name]) 15 | @app.call(env) 16 | end 17 | 18 | def track_deployment_end(machine, ui, start_time) 19 | host = machine.config.orchestrate.tracker_host 20 | return unless host 21 | @logger.debug("Tracking deployment server end to #{host}.") 22 | id = VagrantPlugins::Orchestrate::DEPLOYMENT_ID 23 | server = { 24 | deployment_id: id, 25 | hostname: machine.provider_config.server, 26 | result: "success", 27 | elapsed_seconds: (Time.now - start_time).to_i 28 | } 29 | DeploymentTrackerClient::DefaultApi.put_server(id, server) 30 | rescue => ex 31 | ui.warn("There was an error notifying deployment tracker of server end. See error log for details.") 32 | ui.warn(ex.message) 33 | pp ex 34 | @logger.warn("Error tracking deployment server end for deployment #{id}") 35 | @logger.warn(ex) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/track_server_deployment_start.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ManagedServers 5 | module Action 6 | class TrackServerDeploymentStart 7 | def initialize(app, _env) 8 | @app = app 9 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::track_server_deployment_start") 10 | end 11 | 12 | def call(env) 13 | machine = env[:machine] 14 | env[:start_times] ||= {} 15 | env[:start_times][machine.name] = Time.now 16 | track_deployment_start(machine, env[:ui]) 17 | @app.call(env) 18 | end 19 | 20 | def track_deployment_start(machine, ui) 21 | host = machine.config.orchestrate.tracker_host 22 | return unless host 23 | @logger.debug("Tracking deployment server start to #{host}.") 24 | id = VagrantPlugins::Orchestrate::DEPLOYMENT_ID 25 | server = { 26 | deployment_id: id, 27 | hostname: machine.provider_config.server 28 | } 29 | DeploymentTrackerClient::DefaultApi.post_server(id, server) 30 | rescue => ex 31 | ui.warn("There was an error notifying deployment tracker of server start. See error log for details.") 32 | ui.warn(ex.message) 33 | @logger.warn("Error tracking deployment server start for deployment #{id}") 34 | @logger.warn(ex) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/vagrant-managed-servers/action/upload_status.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ManagedServers 5 | module Action 6 | class UploadStatus 7 | def initialize(app, _env) 8 | @app = app 9 | @logger = Log4r::Logger.new("vagrant_managed_servers::action::upload_status") 10 | end 11 | 12 | def call(env) 13 | upload_status(env[:status], env[:machine], env[:ui]) 14 | 15 | @app.call(env) 16 | end 17 | 18 | def upload_status(status, machine, ui) 19 | source = status.local_path 20 | destination = status.remote_path(machine.config.vm.communicator) 21 | parent_folder = File.split(destination)[0] 22 | machine.communicate.wait_for_ready(5) 23 | @logger.debug("Ensuring vagrant_orchestrate status directory exists") 24 | machine.communicate.sudo("mkdir -p #{parent_folder}") 25 | machine.communicate.sudo("chmod 777 #{parent_folder}") 26 | ui.info("Uploading vagrant orchestrate status to #{destination}") 27 | @logger.debug("Uploading vagrant_orchestrate status") 28 | @logger.debug(" source: #{source}") 29 | @logger.debug(" dest: #{destination}") 30 | machine.communicate.upload(source, destination) 31 | @logger.debug("Setting uploaded file world-writable") 32 | machine.communicate.sudo("chmod 777 #{destination}") 33 | rescue => ex 34 | @logger.error(ex) 35 | ui.warn("An error occurred when trying to upload status to #{machine.name}. Continuing") 36 | ui.warn(ex.message) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-orchestrate/plugin" 2 | 3 | module VagrantPlugins 4 | module Orchestrate 5 | # This returns the path to the source of this plugin. 6 | # 7 | # @return [Pathname] 8 | def self.source_root 9 | @source_root ||= Pathname.new(File.expand_path("../../", __FILE__)) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/action/filtermanaged.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Orchestrate 3 | module Action 4 | class FilterManaged 5 | def initialize(app, _env) 6 | @app = app 7 | end 8 | 9 | def call(env) 10 | machine = env[:machine] 11 | if machine.provider_name == :managed 12 | if (machine.config.orchestrate.filter_managed_commands) && (ENV["VAGRANT_ORCHESTRATE_COMMAND"] != "PUSH") 13 | env[:ui].info("Ignoring action #{env[:machine_action]} for managed server #{machine.name}.") 14 | env[:ui].info("Set `config.orchestrate.filter_managed_commands = false` in your vagrantfile to disable.") 15 | else 16 | @app.call(env) 17 | end 18 | else 19 | @app.call(env) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/action/setcredentials.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Orchestrate 3 | module Action 4 | class SetCredentials 5 | def retrieve_creds(config_creds, ui) 6 | return unless config_creds 7 | 8 | # Use environment variable overrides, or else what was provided in the config file 9 | if ENV["VAGRANT_ORCHESTRATE_USERNAME"] 10 | ui.info("Using VAGRANT_ORCHESTRATE_USERNAME environment variable override") 11 | config_creds.username = ENV["VAGRANT_ORCHESTRATE_USERNAME"].dup 12 | end 13 | if ENV["VAGRANT_ORCHESTRATE_PASSWORD"] 14 | ui.info("Using VAGRANT_ORCHESTRATE_PASSWORD environment variable override") 15 | config_creds.password = ENV["VAGRANT_ORCHESTRATE_PASSWORD"].dup 16 | end 17 | 18 | # Use credentials file to any username or password that is still undiscovered 19 | check_creds_file(config_creds, ui) unless config_creds.username && config_creds.password 20 | 21 | config_creds = maybe_prompt(config_creds, ui) 22 | 23 | [config_creds.username, config_creds.password] 24 | end 25 | 26 | def maybe_prompt(config_creds, ui) 27 | # Only prompt if allowed by config 28 | if config_creds.prompt 29 | config_creds.username ||= prompt_username(ui) 30 | config_creds.password ||= prompt_password(ui) 31 | end 32 | config_creds 33 | end 34 | 35 | def apply_creds(machine, username, password) 36 | [machine.config.winrm, machine.config.ssh].each do |config| 37 | next unless config 38 | config.username = username 39 | config.password = password 40 | end 41 | 42 | # If we're using WinRM, then we'll want to use SMB folder sync, which 43 | # requires creds. 44 | return unless machine.config.vm.communicator == :winrm 45 | machine.config.vm.synced_folders.each do |_id, data| 46 | puts data[:type] 47 | data[:smb_username] = username 48 | data[:smb_password] = password 49 | end 50 | end 51 | 52 | def prompt_username(ui) 53 | default = ENV["USERNAME"] 54 | default ||= ENV["USER"] 55 | default = ENV["USERDOMAIN"] + "\\" + default if ENV["USERDOMAIN"] 56 | username = ui.ask("username? [#{default}] ") 57 | username = default if username.empty? 58 | username 59 | end 60 | 61 | def prompt_password(ui) 62 | ui.ask("password? ", echo: false) 63 | end 64 | 65 | def check_creds_file(config_creds, ui) 66 | file_path = config_creds.file_path 67 | return unless file_path 68 | unless File.exist?(file_path) 69 | @ui.info "Credential file not found at #{file_path}. Prompting user for credentials." 70 | return 71 | end 72 | 73 | begin 74 | creds_yaml = YAML.load(File.read(file_path)) 75 | config_creds.password ||= creds_yaml[:password] || creds_yaml["password"] 76 | config_creds.username ||= creds_yaml[:username] || creds_yaml["username"] 77 | rescue 78 | ui.warn "Credentials file at #{file_path} was not valid YAML. Prompting user for credentials." 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/command/command_mixins.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Orchestrate 3 | module Command 4 | module CommandMixins 5 | # Given an array of vagrant command line args (e.g. the result of calling 6 | # parse_options), filter out unmanaged servers and provide the resulting list. 7 | def filter_unmanaged(argv) 8 | machines = [] 9 | with_target_vms(argv) do |machine| 10 | if machine.provider_name.to_sym == :managed 11 | machines << machine 12 | else 13 | @logger.debug("Skipping #{machine.name} because it doesn't use the :managed provider") 14 | end 15 | end 16 | 17 | if machines.empty? 18 | @env.ui.info("No servers with :managed provider found. Exiting.") 19 | end 20 | 21 | machines 22 | end 23 | 24 | # Delete a file in a way that works on windows 25 | def super_delete(filepath) 26 | # Thanks, Windows. http://alx.github.io/2009/01/27/ruby-wundows-unlink.html 27 | 10.times do 28 | begin 29 | File.delete(filepath) 30 | break 31 | rescue 32 | @logger.warn("Unable to delete file #{filepath}") 33 | sleep 0.05 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/command/init.rb: -------------------------------------------------------------------------------- 1 | require "optparse" 2 | require "vagrant" 3 | 4 | # rubocop:disable Metrics/ClassLength 5 | module VagrantPlugins 6 | module Orchestrate 7 | module Command 8 | class Init < Vagrant.plugin("2", :command) 9 | include Vagrant::Util 10 | 11 | DEFAULT_SHELL_PATH = "{{YOUR_SCRIPT_PATH}}" 12 | DEFAULT_WINRM_USERNAME = "{{YOUR_WINRM_USERNAME}}" 13 | DEFAULT_WINRM_PASSWORD = "{{YOUR_WINRM_PASSWORD}}" 14 | DEFAULT_SSH_USERNAME = "{{YOUR_SSH_USERNAME}}" 15 | DEFAULT_SSH_PRIVATE_KEY_PATH = "{{YOUR_SSH_PRIVATE_KEY_PATH}}" 16 | DEFAULT_PLUGINS = ["vagrant-orchestrate", "vagrant-managed-servers"] 17 | 18 | # rubocop:disable MethodLength 19 | def execute 20 | options = {} 21 | 22 | options[:provisioners] = [] 23 | options[:servers] = [] 24 | options[:environments] = [] 25 | options[:plugins] = DEFAULT_PLUGINS 26 | options[:puppet_librarian_puppet] = false 27 | options[:puppet_hiera] = true 28 | options[:git] = true 29 | 30 | opts = OptionParser.new do |o| 31 | o.banner = "Usage: vagrant orchestrate init [options]" 32 | o.separator "" 33 | o.separator "Options:" 34 | o.separator "" 35 | 36 | o.on("--provision-with x,y,z", Array, "Init only certain provisioners, by type.") do |list| 37 | options[:provisioners] = list 38 | end 39 | 40 | o.on("--shell", "Shorthand for --provision-with shell") do 41 | options[:provisioners] << "shell" 42 | end 43 | 44 | o.on("--shell-paths x,y,z", Array, 45 | "Comma-separated list of shell scripts to run on provision. Only with --shell") do |list| 46 | options[:shell_paths] = list 47 | end 48 | 49 | o.on("--shell-inline command", String, "Inline script to run. Only with --shell") do |c| 50 | options[:shell_inline] = c 51 | end 52 | 53 | o.on("--puppet", "Shorthand for '--provision-with puppet'") do 54 | options[:provisioners] << "puppet" 55 | end 56 | 57 | o.on("--[no-]puppet-hiera", "Include templates for hiera. Only with --puppet") do |p| 58 | options[:puppet_hiera] = p 59 | end 60 | 61 | o.on("--[no-]puppet-librarian-puppet", 62 | "Include a Puppetfile and the vagrant-librarian-puppet plugin. Only with --puppet") do |p| 63 | options[:puppet_librarian_puppet] = p 64 | end 65 | 66 | o.on("--ssh-username USERNAME", String, "The username for communicating over ssh") do |u| 67 | options[:ssh_username] = u 68 | end 69 | 70 | o.on("--ssh-password PASSWORD", String, "The password for communicating over ssh") do |p| 71 | options[:ssh_password] = p 72 | end 73 | 74 | o.on("--ssh-private-key-path PATH", String, "Paths to the private key for communinicating over ssh") do |k| 75 | options[:ssh_private_key_path] = k 76 | end 77 | 78 | o.on("--winrm", "Use the winrm communicator") do 79 | options[:communicator] = "winrm" 80 | options[:plugins] << "vagrant-winrm-s" 81 | end 82 | 83 | o.on("--winrm-username USERNAME", String, "The username for communicating with winrm") do |u| 84 | options[:winrm_username] = u 85 | end 86 | 87 | o.on("--winrm-password PASSWORD", String, "The password for communicating with winrm") do |p| 88 | options[:winrm_password] = p 89 | end 90 | 91 | o.on("--plugins x,y,z", Array, "A comma separated list of vagrant plugins to be installed") do |p| 92 | options[:plugins] += p 93 | end 94 | 95 | o.on("--servers x,y,z", Array, "A CSV list of FQDNs to target managed servers") do |list| 96 | options[:servers] = list 97 | end 98 | 99 | o.on("--environments x,y,z", Array, "A CSV list of environments. Takes precedence over --servers") do |list| 100 | options[:environments] = list 101 | end 102 | 103 | o.on("--[no-]git", "Include useful templates for working in a git repository. Default is true.") 104 | 105 | o.on("-f", "--force", "Force overwriting of files") do 106 | options[:force] = true 107 | end 108 | 109 | o.on("--credentials-prompt", "Prompt for credentials when performing orchestrate operations") do 110 | options[:creds_prompt] = true 111 | end 112 | 113 | cfpmsg = "The path to a yaml file containing :username and :password fields to use with vagrant orchestrate" 114 | o.on("--credentials-file-path FILEPATH", String, cfpmsg) do |file_path| 115 | options[:creds_file_path] = file_path 116 | end 117 | 118 | o.on("--deployment-tracker-host host", String, "Fully qualified URL of deployment-tracker instance") do |t| 119 | options[:tracker_host] = t 120 | end 121 | end 122 | 123 | argv = parse_options(opts) 124 | return unless argv 125 | 126 | options[:shell_paths] ||= options[:shell_inline] ? [] : [DEFAULT_SHELL_PATH] 127 | options[:winrm_username] ||= DEFAULT_WINRM_USERNAME 128 | options[:winrm_password] ||= DEFAULT_WINRM_PASSWORD 129 | options[:communicator] ||= "ssh" 130 | options[:ssh_username] ||= DEFAULT_SSH_USERNAME 131 | options[:ssh_private_key_path] ||= DEFAULT_SSH_PRIVATE_KEY_PATH unless options[:ssh_password] 132 | 133 | init_puppet options 134 | init_environments options 135 | init_vagrant_files options 136 | init_git options 137 | @env.ui.info(I18n.t("vagrant.commands.init.success"), prefix: false) 138 | 139 | # Success, exit status 0 140 | 0 141 | end 142 | # rubocop:enable MethodLength 143 | 144 | private 145 | 146 | def init_puppet(options) 147 | return unless options[:provisioners].include? "puppet" 148 | 149 | FileUtils.mkdir_p(File.join(@env.cwd, "puppet")) 150 | if options[:puppet_librarian_puppet] 151 | contents = TemplateRenderer.render(Orchestrate.source_root.join("templates/puppet/Puppetfile")) 152 | write_file File.join("puppet", "Puppetfile"), contents, options 153 | FileUtils.mkdir_p(File.join(@env.cwd, "puppet", "modules")) 154 | write_file(File.join(@env.cwd, "puppet", "modules", ".gitignore"), "*", options) if options[:git] 155 | options[:plugins] << "vagrant-librarian-puppet" 156 | end 157 | 158 | if options[:puppet_hiera] 159 | contents = TemplateRenderer.render(Orchestrate.source_root.join("templates/puppet/hiera.yaml")) 160 | write_file(File.join("puppet", "hiera.yaml"), contents, options) 161 | FileUtils.mkdir_p(File.join(@env.cwd, "puppet", "hieradata")) 162 | contents = TemplateRenderer.render(Orchestrate.source_root.join("templates/puppet/hiera/common.yaml")) 163 | write_file(File.join(@env.cwd, "puppet", "hieradata", "common.yaml"), contents, options) 164 | end 165 | 166 | FileUtils.mkdir_p(File.join(@env.cwd, "puppet", "manifests")) 167 | write_file(File.join(@env.cwd, "puppet", "manifests", "default.pp"), 168 | "# Your puppet code goes here", options) 169 | end 170 | 171 | def init_environments(options) 172 | environments = options[:environments] 173 | return unless environments.any? 174 | 175 | contents = TemplateRenderer.render(Orchestrate.source_root.join("templates/environment/servers.json"), 176 | environments: environments) 177 | write_file("servers.json", contents, options) 178 | @env.ui.info("You've created an environment-aware configuration.") 179 | @env.ui.info("To complete the process, you need to do the following: ") 180 | @env.ui.info(" 1. Add the target servers to servers.json") 181 | @env.ui.info(" 2. Commit your changes") 182 | @env.ui.info(" 3. Create a git branch for each environment") 183 | environments.each do |env| 184 | @env.ui.info(" git branch #{env}") 185 | end 186 | end 187 | 188 | def init_vagrant_files(options) 189 | contents = TemplateRenderer.render(Orchestrate.source_root.join("templates/vagrant/Vagrantfile"), 190 | provisioners: options[:provisioners], shell_paths: options[:shell_paths], 191 | shell_inline: options[:shell_inline], 192 | puppet_librarian_puppet: options[:puppet_librarian_puppet], 193 | puppet_hiera: options[:puppet_hiera], communicator: options[:communicator], 194 | winrm_username: options[:winrm_username], 195 | winrm_password: options[:winrm_password], 196 | ssh_username: options[:ssh_username], ssh_password: options[:ssh_password], 197 | ssh_private_key_path: options[:ssh_private_key_path], 198 | servers: options[:servers], environments: options[:environments], 199 | creds_prompt: options[:creds_prompt], tracker_host: options[:tracker_host] 200 | ) 201 | write_file("Vagrantfile", contents, options) 202 | 203 | contents = TemplateRenderer.render(Orchestrate.source_root.join("templates/vagrant/.vagrantplugins"), 204 | plugins: options[:plugins] 205 | ) 206 | write_file(".vagrantplugins", contents, options) 207 | 208 | FileUtils.cp(Orchestrate.source_root.join("templates", "vagrant", "dummy.box"), 209 | File.join(@env.cwd, "dummy.box")) 210 | end 211 | 212 | def init_git(options) 213 | return unless options[:git] 214 | 215 | gitignore_path = File.join(@env.cwd, ".gitignore") 216 | contents = ::IO.readlines(gitignore_path) if File.exist?(gitignore_path) 217 | contents ||= [] 218 | 219 | open(gitignore_path, "a") do |f| 220 | %w( .vagrant/ ).each do |path| 221 | f.puts path unless contents.include?(path + "\n") 222 | end 223 | end 224 | end 225 | 226 | def write_file(filename, contents, options) 227 | save_path = Pathname.new(filename).expand_path(@env.cwd) 228 | save_path.delete if save_path.exist? && options[:force] 229 | fail Vagrant::Errors::VagrantfileExistsError if save_path.exist? 230 | 231 | begin 232 | save_path.open("w+") do |f| 233 | f.write(contents) 234 | end 235 | rescue Errno::EACCES 236 | raise Vagrant::Errors::VagrantfileWriteError 237 | end 238 | end 239 | end 240 | end 241 | end 242 | end 243 | # rubocop:enable Metrics/ClassLength 244 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/command/push.rb: -------------------------------------------------------------------------------- 1 | require "English" 2 | require "optparse" 3 | require "vagrant" 4 | require_relative "../../vagrant-managed-servers/action" 5 | require "vagrant-orchestrate/action/setcredentials" 6 | require "vagrant-orchestrate/repo_status" 7 | require_relative "command_mixins" 8 | require "deployment-tracker-client" 9 | require "log4r/outputter/deployment_tracker_outputter" 10 | 11 | # Borrowed from http://stackoverflow.com/questions/12374645/splitting-an-array-into-equal-parts-in-ruby 12 | class Array 13 | def in_groups(num_groups) 14 | return [] if num_groups == 0 15 | slice_size = (size / Float(num_groups)).ceil 16 | each_slice(slice_size).to_a 17 | end 18 | end 19 | 20 | module VagrantPlugins 21 | module Orchestrate 22 | module Command 23 | class Push < Vagrant.plugin("2", :command) 24 | include Vagrant::Util 25 | include CommandMixins 26 | 27 | @logger = Log4r::Logger.new("vagrant_orchestrate::command::push") 28 | 29 | # rubocop:disable MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 30 | def execute 31 | options = {} 32 | options[:force] = @env.vagrantfile.config.orchestrate.force_push 33 | options[:provision] = true 34 | 35 | opts = OptionParser.new do |o| 36 | o.banner = "Usage: vagrant orchestrate push" 37 | o.separator "" 38 | 39 | o.on("--[no-]provision", "Enable or disable provisioning. Default is true") do |p| 40 | options[:provision] = p 41 | end 42 | 43 | o.on("--reboot", "Reboot a managed server after the provisioning step") do 44 | options[:reboot] = true 45 | end 46 | 47 | o.on("--strategy strategy", "The orchestration strategy to use. Default is serial") do |v| 48 | options[:strategy] = v 49 | end 50 | 51 | o.on("-f", "--force", "Suppress prompting in between groups") do 52 | options[:force] = true 53 | end 54 | end 55 | 56 | argv = parse_options(opts) 57 | return unless argv 58 | 59 | guard_clean 60 | 61 | machines = filter_unmanaged(argv) 62 | return 0 if machines.empty? 63 | 64 | @start_time = Time.now 65 | 66 | retrieve_creds(machines) if @env.vagrantfile.config.orchestrate.credentials 67 | 68 | # Write the status file to disk so that it can be used as part of the 69 | # push action. 70 | status = RepoStatus.new(@env.root_path) 71 | status.write(@env.tmp_path) 72 | options[:status] = status 73 | 74 | @env.action_runner.run(VagrantPlugins::ManagedServers::Action::InitDeploymentTracker, 75 | tracker_host: @env.vagrantfile.config.orchestrate.tracker_host, 76 | tracker_logging_enabled: @env.vagrantfile.config.orchestrate.tracker_logging_enabled, 77 | ui: @env.ui) 78 | @env.action_runner.run(VagrantPlugins::ManagedServers::Action::TrackDeploymentStart, 79 | tracker_host: @env.vagrantfile.config.orchestrate.tracker_host, 80 | status: status, 81 | args: ARGV.drop(2).join(" ")) 82 | 83 | options[:parallel] = true 84 | strategy = options[:strategy] || @env.vagrantfile.config.orchestrate.strategy 85 | @env.ui.info("Pushing to managed servers using #{strategy} strategy.") 86 | 87 | # Handle a couple of them more tricky edges. 88 | strategy = :serial if machines.size == 1 89 | strategy = :half_half if strategy.to_sym == :canary_half_half && machines.size == 2 90 | 91 | begin 92 | case strategy.to_sym 93 | when :serial 94 | options[:parallel] = false 95 | result = deploy(options, machines) 96 | when :parallel 97 | result = deploy(options, machines) 98 | when :canary 99 | # A single canary server and then the rest 100 | result = deploy(options, machines.take(1), machines.drop(1)) 101 | when :half_half 102 | # Split into two (almost) equal groups 103 | groups = split(machines) 104 | result = deploy(options, groups.first, groups.last) 105 | when :canary_half_half 106 | # A single canary and then two equal groups 107 | canary = machines.take(1) 108 | groups = split(machines.drop(1)) 109 | result = deploy(options, canary, groups.first, groups.last) 110 | else 111 | @env.ui.error("Invalid deployment strategy specified") 112 | result = false 113 | end 114 | ensure 115 | @env.action_runner.run(VagrantPlugins::ManagedServers::Action::TrackDeploymentEnd, 116 | tracker_host: @env.vagrantfile.config.orchestrate.tracker_host, 117 | start_time: @start_time, 118 | success: result) 119 | end 120 | 121 | return 1 unless result 122 | 0 123 | end 124 | # rubocop:enable MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 125 | 126 | def split(machines) 127 | groups = machines.in_groups(2) 128 | # Move an item from the first to second group if they are unbalanced so that 129 | # the smaller group is pushed to first. 130 | groups.last.unshift(groups.first.pop) if groups.any? && groups.first.size > groups.last.size 131 | groups 132 | end 133 | 134 | # rubocop:disable MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 135 | def deploy(options, *groups) 136 | groups.select! { |g| g.size > 0 } 137 | groups.each_with_index do |machines, index| 138 | next if machines.empty? 139 | if groups.size > 1 140 | @env.ui.info("Orchestrating push to group number #{index + 1} of #{groups.size}.") 141 | @env.ui.info(" -- Hosts: #{machines.collect { |m| m.name.to_s }.join(',')}") 142 | end 143 | ENV["VAGRANT_ORCHESTRATE_COMMAND"] = "PUSH" 144 | begin 145 | batchify(machines, :push, options) 146 | ensure 147 | @logger.debug("Finished orchestrating push to group number #{index + 1} of #{groups.size}.") 148 | status_source = options[:status].local_path 149 | super_delete(status_source) if File.exist?(status_source) 150 | ENV.delete "VAGRANT_ORCHESTRATE_COMMAND" 151 | end 152 | 153 | # Don't prompt on the last group, that would be annoying 154 | if index == groups.size - 1 || options[:force] 155 | @logger.debug("Suppressing prompt because --force specified.") if options[:force] 156 | else 157 | return false unless prompt_for_continue 158 | end 159 | end 160 | end 161 | # rubocop:enable MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 162 | 163 | def prompt_for_continue 164 | result = @env.ui.ask("Deployment paused for manual review. Would you like to continue? (y/n) ") 165 | if result.upcase != "Y" 166 | @env.ui.info("Deployment push action cancelled by user") 167 | return false 168 | end 169 | true 170 | end 171 | 172 | def batchify(machines, action, options) 173 | @env.batch(options[:parallel]) do |batch| 174 | machines.each do |machine| 175 | # This is necessary to disable the low level provisioning in the 176 | # Vagrant builtin provisioner. 177 | options[:provision_enabled] = false unless options[:provision] 178 | batch.action(machine, action, options) 179 | end 180 | end 181 | end 182 | 183 | def retrieve_creds(machines) 184 | creds = VagrantPlugins::Orchestrate::Action::SetCredentials.new 185 | (username, password) = creds.retrieve_creds(@env.vagrantfile.config.orchestrate.credentials, @env.ui) 186 | 187 | # Apply the credentials to the machine info, or back out if we were unable to procure them. 188 | if username && password 189 | machines.each do |machine| 190 | creds.apply_creds(machine, username, password) 191 | end 192 | else 193 | @env.ui.warn "Vagrant-orchestrate did find any credentials. Continuing with default credentials." 194 | end 195 | end 196 | 197 | def guard_clean 198 | return if ENV["VAGRANT_ORCHESTRATE_NO_GUARD_CLEAN"] || \ 199 | @env.vagrantfile.config.orchestrate.disable_commit_guard 200 | message = "ERROR!\nThere are files that need to be committed first." 201 | RepoStatus.clean? && RepoStatus.committed? && !RepoStatus.untracked? || abort(message) 202 | end 203 | end 204 | end 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/command/root.rb: -------------------------------------------------------------------------------- 1 | require "optparse" 2 | require "vagrant" 3 | 4 | module VagrantPlugins 5 | module Orchestrate 6 | module Command 7 | class Root < Vagrant.plugin(2, :command) 8 | def self.synopsis 9 | "Orchestrate provsioning of managed servers across environments" 10 | end 11 | 12 | def initialize(argv, env) 13 | super 14 | 15 | @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) 16 | 17 | @subcommands = Vagrant::Registry.new 18 | 19 | @subcommands.register(:init) do 20 | require_relative "init" 21 | Init 22 | end 23 | 24 | @subcommands.register(:push) do 25 | require_relative "push" 26 | Push 27 | end 28 | 29 | @subcommands.register(:status) do 30 | require_relative "status" 31 | Status 32 | end 33 | end 34 | 35 | def execute 36 | if @main_args.include?("-h") || @main_args.include?("--help") 37 | # Print the help for all the box commands. 38 | return help 39 | end 40 | 41 | # If we reached this far then we must have a subcommand. If not, 42 | # then we also just print the help and exit. 43 | command_class = @subcommands.get(@sub_command.to_sym) if @sub_command 44 | return help if !command_class || !@sub_command 45 | @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") 46 | 47 | # Initialize and execute the command class 48 | command_class.new(@sub_args, @env).execute 49 | end 50 | 51 | # Prints the help out for this command 52 | def help 53 | opts = OptionParser.new do |o| 54 | o.banner = "Usage: vagrant orchestrate []" 55 | o.separator "" 56 | o.separator "Available subcommands:" 57 | 58 | # Add the available subcommands as separators in order to print them 59 | # out as well. 60 | keys = [] 61 | @subcommands.each { |key, _value| keys << key.to_s } 62 | 63 | keys.sort.each do |key| 64 | o.separator " #{key}" 65 | end 66 | 67 | o.separator "" 68 | o.separator "For help on any individual subcommand run `vagrant orchestrate -h`" 69 | end 70 | 71 | @env.ui.info(opts.help, prefix: false) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/command/status.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "optparse" 3 | require "vagrant" 4 | require "vagrant-managed-servers/action/download_status" 5 | require_relative "../../vagrant-managed-servers/action" 6 | require "vagrant-orchestrate/repo_status" 7 | require_relative "command_mixins" 8 | 9 | module VagrantPlugins 10 | module Orchestrate 11 | module Command 12 | class Status < Vagrant.plugin("2", :command) 13 | include Vagrant::Util 14 | include CommandMixins 15 | 16 | @logger = Log4r::Logger.new("vagrant_orchestrate::command::status") 17 | 18 | def execute 19 | opts = OptionParser.new do |o| 20 | o.banner = "Usage: vagrant orchestrate status" 21 | o.separator "" 22 | end 23 | 24 | argv = parse_options(opts) 25 | return unless argv 26 | 27 | machines = filter_unmanaged(argv) 28 | return 0 if machines.empty? 29 | 30 | print_status machines 31 | end 32 | 33 | def print_status(machines) 34 | # There is some detail output fromt he communicator.download that I 35 | # don't want to suppress, but I also don't want it to be interspersed 36 | # with the actual status information. Let's buffer the status output. 37 | ENV["VAGRANT_ORCHESTRATE_STATUS"] = "" 38 | @logger.debug("About to download machine status") 39 | options = {} 40 | parallel = true 41 | local_files = [] 42 | @env.batch(parallel) do |batch| 43 | machines.each do |machine| 44 | status = RepoStatus.new(@env.root_path) 45 | options[:remote_file_path] = status.remote_path(machine.config.vm.communicator) 46 | options[:local_file_path] = File.join(@env.tmp_path, "#{machine.name}_status") 47 | local_files << options[:local_file_path] 48 | batch.action(machine, :download_status, options) 49 | end 50 | end 51 | @env.ui.info("Current managed server states:\n") 52 | @env.ui.info(ENV["VAGRANT_ORCHESTRATE_STATUS"].split("\n").sort.join("\n")) 53 | ensure 54 | local_files.each do |local| 55 | super_delete(local) if File.exist?(local) 56 | end 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/config.rb: -------------------------------------------------------------------------------- 1 | require "vagrant" 2 | require "yaml" 3 | 4 | module VagrantPlugins 5 | module Orchestrate 6 | class Config < Vagrant.plugin(2, :config) 7 | attr_accessor :filter_managed_commands 8 | attr_accessor :strategy 9 | attr_accessor :force_push 10 | attr_accessor :tracker_host 11 | attr_accessor :tracker_logging_enabled 12 | attr_accessor :credentials 13 | attr_accessor :disable_commit_guard 14 | attr_accessor :take_synced_folder_ownership 15 | 16 | def initialize 17 | @filter_managed_commands = UNSET_VALUE 18 | @strategy = UNSET_VALUE 19 | @force_push = UNSET_VALUE 20 | @tracker_host = UNSET_VALUE 21 | @tracker_logging_enabled = UNSET_VALUE 22 | @disable_commit_guard = UNSET_VALUE 23 | @take_synced_folder_ownership = UNSET_VALUE 24 | @credentials = Credentials.new 25 | end 26 | 27 | def credentials 28 | yield @credentials if block_given? 29 | @credentials 30 | end 31 | 32 | # It was a little hard to dig up, but this method gets called on the more general 33 | # config object, with the more specific config as the argument. 34 | # https://github.com/mitchellh/vagrant/blob/master/lib/vagrant/config/v2/loader.rb 35 | def merge(new_config) 36 | super.tap do |result| 37 | if new_config.credentials.unset? 38 | result.credentials = @credentials 39 | elsif @credentials.unset? 40 | result.credentials = new_config.credentials 41 | else 42 | result.credentials = @credentials.merge(new_config.credentials) 43 | end 44 | end 45 | end 46 | 47 | # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 48 | def finalize! 49 | @filter_managed_commands = false if @filter_managed_commands == UNSET_VALUE 50 | @strategy = :serial if @strategy == UNSET_VALUE 51 | @force_push = false if @force_push == UNSET_VALUE 52 | @tracker_host = nil if @tracker_host == UNSET_VALUE 53 | @tracker_logging_enabled = true if @tracker_logging_enabled == UNSET_VALUE 54 | @disable_commit_guard = false if @disable_commit_guard == UNSET_VALUE 55 | @take_synced_folder_ownership = true if @take_synced_folder_ownership == UNSET_VALUE 56 | @credentials = nil if @credentials.unset? 57 | @credentials.finalize! if @credentials 58 | end 59 | # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity 60 | 61 | class Credentials 62 | # Same as Vagrant does to distinguish uninitialized variables and intentional assignments 63 | # to Ruby's nil, we just have to define ourselves because we're in different scope 64 | UNSET_VALUE = ::Vagrant::Plugin::V2::Config::UNSET_VALUE 65 | 66 | attr_accessor :prompt 67 | attr_accessor :file_path 68 | attr_accessor :username 69 | attr_accessor :password 70 | 71 | def initialize 72 | @prompt = UNSET_VALUE 73 | @file_path = UNSET_VALUE 74 | @username = UNSET_VALUE 75 | @password = UNSET_VALUE 76 | @unset = nil 77 | end 78 | 79 | def unset? 80 | @unset || [@prompt, @file_path, @username, @password] == [UNSET_VALUE, UNSET_VALUE, UNSET_VALUE, UNSET_VALUE] 81 | end 82 | 83 | # Merge needs to be implemented here because this class doesn't get to 84 | # to extend Vagrant.plugin(2, :config), and it would be pretty surprising 85 | # if credentials configuration defined at different levels couldn't be merged 86 | def merge(new_config) 87 | result = dup 88 | unless new_config.unset? 89 | result.prompt = new_config.prompt unless new_config.prompt == UNSET_VALUE 90 | result.file_path = new_config.file_path unless new_config.file_path == UNSET_VALUE 91 | result.username = new_config.username unless new_config.username == UNSET_VALUE 92 | result.password = new_config.password unless new_config.password == UNSET_VALUE 93 | end 94 | result 95 | end 96 | 97 | def finalize! 98 | @unset = unset? 99 | @prompt = nil if @prompt == UNSET_VALUE 100 | @file_path = nil if @file_path == UNSET_VALUE 101 | @username = nil if @username == UNSET_VALUE 102 | @password = nil if @password == UNSET_VALUE 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/plugin.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-orchestrate/action/filtermanaged" 2 | require "vagrant-orchestrate/action/setcredentials" 3 | require "securerandom" 4 | 5 | begin 6 | require "vagrant" 7 | rescue LoadError 8 | raise "The Vagrant Orchestrate plugin must be run within Vagrant." 9 | end 10 | 11 | # This is a sanity check to make sure no one is attempting to install 12 | # this into an early Vagrant version. 13 | if Vagrant::VERSION < "1.6.0" 14 | fail "The Vagrant Orchestrate plugin is only compatible with Vagrant 1.6+" 15 | end 16 | 17 | module VagrantPlugins 18 | module Orchestrate 19 | DEPLOYMENT_ID = SecureRandom.uuid 20 | class Plugin < Vagrant.plugin("2") 21 | name "Orchestrate" 22 | description <<-DESC 23 | This plugin installs commands that make pushing changes to vagrant-managed-servers easy. 24 | DESC 25 | 26 | config "orchestrate" do 27 | require_relative "config" 28 | Config 29 | end 30 | 31 | command(:orchestrate) do 32 | setup_i18n 33 | setup_logging 34 | 35 | require_relative "command/root" 36 | Command::Root 37 | end 38 | 39 | action_hook(:orchestrate, :machine_action_up) do |hook| 40 | hook.prepend Action::FilterManaged 41 | end 42 | 43 | action_hook(:orchestrate, :machine_action_provision) do |hook| 44 | hook.prepend Action::FilterManaged 45 | end 46 | 47 | action_hook(:orchestrate, :machine_action_destroy) do |hook| 48 | hook.prepend Action::FilterManaged 49 | end 50 | 51 | action_hook(:orchestrate, :machine_action_reload) do |hook| 52 | hook.prepend Action::FilterManaged 53 | end 54 | 55 | # This initializes the internationalization strings. 56 | def self.setup_i18n 57 | I18n.load_path << File.expand_path("locales/en.yml", Orchestrate.source_root) 58 | I18n.reload! 59 | end 60 | 61 | # This sets up our log level to be whatever VAGRANT_LOG is. 62 | def self.setup_logging 63 | require "log4r" 64 | 65 | level = nil 66 | begin 67 | level = Log4r.const_get(ENV["VAGRANT_LOG"].upcase) 68 | rescue NameError 69 | # This means that the logging constant wasn't found, 70 | # which is fine. We just keep `level` as `nil`. But 71 | # we tell the user. 72 | level = nil 73 | end 74 | 75 | # Some constants, such as "true" resolve to booleans, so the 76 | # above error checking doesn't catch it. This will check to make 77 | # sure that the log level is an integer, as Log4r requires. 78 | level = nil unless level.is_a?(Integer) 79 | 80 | # Set the logging level on all "vagrant" namespaced 81 | # logs as long as we have a valid level. 82 | @logger = Log4r::Logger.new("vagrant_orchestrate").tap do |logger| 83 | logger.outputters = Log4r::Outputter.stderr 84 | logger.level = level || 6 85 | end 86 | end 87 | 88 | def self.read_git_branch 89 | @logger.debug("Reading git branch") 90 | if ENV.key?("GIT_BRANCH") 91 | git_branch = ENV["GIT_BRANCH"].split("/").last 92 | @logger.debug("Read git branch #{git_branch} from GIT_BRANCH environment variable") 93 | else 94 | command = "git rev-parse --abbrev-ref HEAD 2>&1" 95 | git_branch = `#{command}`.chomp 96 | if git_branch.include? "fatal" 97 | @logger.error("Unable to determine git branch `#{command}`. Is this a git repo?") 98 | git_branch = nil 99 | else 100 | @logger.debug("Read git branch #{git_branch} using `#{command}`") 101 | end 102 | end 103 | ENV["VAGRANT_ORCHESTRATE_GIT_BRANCH"] = git_branch 104 | git_branch 105 | end 106 | 107 | def self.load_servers_for_branch 108 | setup_logging 109 | 110 | git_branch = read_git_branch 111 | return [] if git_branch.nil? 112 | 113 | begin 114 | fail "servers.json not found" unless File.exist?("servers.json") 115 | @logger.debug("Reading servers.json") 116 | contents = IO.read("servers.json") 117 | @logger.debug("Read servers.json:\n: #{contents}") 118 | 119 | environments = JSON.parse(contents)["environments"] 120 | if environments.key? git_branch 121 | return environments[git_branch]["servers"] 122 | else 123 | @logger.info("No environment found for #{git_branch}, no servers loaded.") 124 | return [] 125 | end 126 | rescue StandardError => ex 127 | # Don't break the user's whole vagrantfile if we can't load the environment 128 | @logger.error(ex.message) 129 | return [] 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/repo_status.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module VagrantPlugins 4 | module Orchestrate 5 | class RepoStatus 6 | attr_reader :last_sync, :root_path 7 | attr_accessor :local_path 8 | 9 | def initialize(root_path) 10 | @last_sync = Time.now.utc # Managed servers could be in different timezones 11 | @local_path = nil # The fully qualified path to the root of the repo 12 | @root_path = root_path 13 | end 14 | 15 | def ref 16 | # Env vars are here only for testing, since vagrant-spec is executed from 17 | # a temp directory and can't use git to get repository information 18 | @ref ||= ENV["VAGRANT_ORCHESTRATE_STATUS_TEST_REF"] 19 | @ref ||= `git log --pretty=format:'%H' --abbrev-commit -1` 20 | @ref 21 | end 22 | 23 | def remote_origin_url 24 | @remote_origin_url ||= ENV["VAGRANT_ORCHESTRATE_STATUS_TEST_REMOTE_ORIGIN_URL"] 25 | @remote_origin_url ||= `git config --get remote.origin.url 2>#{File::NULL}`.chomp 26 | @remote_origin_url 27 | end 28 | 29 | def repo 30 | @repo ||= ENV["VAGRANT_ORCHESTRATE_STATUS_TEST_REPO"] 31 | @repo ||= File.basename(`git rev-parse --show-toplevel 2>#{File::NULL}`.chomp) 32 | # This might not be a git repo, and that should still be supported. 33 | @repo = File.basename(root_path) if @repo.nil? || @repo.empty? 34 | @repo 35 | end 36 | 37 | def branch 38 | @branch ||= ENV["VAGRANT_ORCHESTRATE_STATUS_TEST_BRANCH"] 39 | @branch ||= `git rev-parse --abbrev-ref HEAD`.chomp 40 | @branch 41 | end 42 | 43 | def user 44 | user = ENV["USER"] || ENV["USERNAME"] || "unknown" 45 | user = ENV["USERDOMAIN"] + "\\" + user if ENV["USERDOMAIN"] 46 | 47 | @user ||= user 48 | @user 49 | end 50 | 51 | def to_json 52 | contents = { 53 | repo: repo, 54 | remote_url: remote_origin_url, 55 | ref: ref, 56 | user: user, 57 | last_sync: last_sync 58 | } 59 | JSON.pretty_generate(contents) 60 | end 61 | 62 | def write(tmp_path) 63 | @local_path = File.join(tmp_path, "vagrant_orchestrate_status") 64 | File.write(@local_path, to_json) 65 | end 66 | 67 | # The path to where this should be stored on a remote machine, inclusive 68 | # of the file name. 69 | def remote_path(communicator) 70 | if communicator == :winrm 71 | File.join("c:", "programdata", "vagrant_orchestrate", repo) 72 | else 73 | File.join("/var", "state", "vagrant_orchestrate", repo) 74 | end 75 | end 76 | 77 | def self.clean? 78 | `git diff --exit-code 2>&1` 79 | $CHILD_STATUS == 0 80 | end 81 | 82 | def self.committed? 83 | `git diff-index --quiet --cached HEAD 2>&1` 84 | $CHILD_STATUS == 0 85 | end 86 | 87 | # Return whether there are any untracked files in the git repo 88 | def self.untracked? 89 | output = `git ls-files --other --exclude-standard --directory --no-empty-directory 2>&1` 90 | # This command lists untracked files. There are untracked files if the ouput is not empty. 91 | !output.empty? 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/vagrant-orchestrate/version.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Orchestrate 3 | VERSION = "0.8.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | vagrant_orchestrate: 3 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | RSpec.configure do |config| 8 | config.treat_symbols_as_metadata_keys_with_true_values = true 9 | config.run_all_when_everything_filtered = true 10 | config.filter_run :focus 11 | 12 | # Run specs in random order to surface order dependencies. If you find an 13 | # order dependency and want to debug it, you can fix the order by providing 14 | # the seed, which is printed after each run. 15 | # --seed 1234 16 | config.order = "random" 17 | end 18 | 19 | def capture_stdout(&_block) 20 | original_stdout = $stdout 21 | $stdout = fake = StringIO.new 22 | begin 23 | yield 24 | ensure 25 | $stdout = original_stdout 26 | end 27 | fake.string 28 | end 29 | -------------------------------------------------------------------------------- /spec/vagrant-orchestrate/command/init_spec.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-orchestrate/command/init" 2 | require "vagrant-spec/unit" 3 | 4 | describe VagrantPlugins::Orchestrate::Command::Init do 5 | include_context "vagrant-unit" 6 | 7 | let(:base_argv) { ["-f"] } 8 | let(:argv) { [] } 9 | let(:iso_env) do 10 | env = isolated_environment 11 | # We need to load an empty vagrantfile in order for things to be initialized 12 | # properly 13 | env.vagrantfile("") 14 | env.create_vagrant_env ui_class: ui_class 15 | end 16 | 17 | let(:ui_class) { nil } 18 | 19 | subject { described_class.new(base_argv + argv, iso_env) } 20 | 21 | ["-h", "--help"].each do |arg| 22 | describe "init help message #{arg}" do 23 | let(:argv) { ["init", arg] } 24 | let(:ui_class) { Vagrant::UI::Basic } 25 | it "shows help" do 26 | output = capture_stdout { subject.execute } 27 | expect(output).to include("Usage: vagrant orchestrate init [options]") 28 | end 29 | end 30 | end 31 | 32 | describe "no parameters" do 33 | it "creates basic vagrantfile" do 34 | subject.execute 35 | expect(Dir.entries(iso_env.cwd)).to include("Vagrantfile") 36 | end 37 | end 38 | 39 | context "shell provisioner" do 40 | describe "basic operation" do 41 | let(:argv) { ["--shell"] } 42 | it "creates a vagrantfile with default shell path" do 43 | subject.execute 44 | expect(iso_env.vagrantfile.config.vm.provisioners.first.type).to eq(:shell) 45 | expect(iso_env.vagrantfile.config.vm.provisioners.first.config.path).to eq(described_class::DEFAULT_SHELL_PATH) 46 | expect(iso_env.vagrantfile.config.vm.provisioners.count).to eq(1) 47 | end 48 | end 49 | 50 | describe "shell path" do 51 | let(:argv) { ["--shell", "--shell-paths", "foo.sh"] } 52 | it "creates a vagrantfile with custom shell path" do 53 | subject.execute 54 | expect(iso_env.vagrantfile.config.vm.provisioners.first.type).to eq(:shell) 55 | expect(iso_env.vagrantfile.config.vm.provisioners.first.config.path).to eq("foo.sh") 56 | end 57 | end 58 | 59 | describe "shell inline" do 60 | let(:argv) { ["--shell", "--shell-inline", "echo Hello, World!"] } 61 | it "is passed through" do 62 | subject.execute 63 | expect(iso_env.vagrantfile.config.vm.provisioners.first.config.inline).to eq("echo Hello, World!") 64 | end 65 | end 66 | 67 | describe "multiple shell paths" do 68 | let(:argv) { ["--shell", "--shell-paths", "foo.sh,bar.sh"] } 69 | it "creates a vagrantfile with custom shell path" do 70 | subject.execute 71 | expect(iso_env.vagrantfile.config.vm.provisioners.first.type).to eq(:shell) 72 | expect(iso_env.vagrantfile.config.vm.provisioners.first.config.path).to eq("foo.sh") 73 | expect(iso_env.vagrantfile.config.vm.provisioners[1].config.path).to eq("bar.sh") 74 | end 75 | end 76 | end 77 | 78 | context "puppet" do 79 | describe "basic operation" do 80 | let(:argv) { ["--provision-with", "puppet"] } 81 | it "creates a vagrantfile with a puppet provisioner" do 82 | subject.execute 83 | expect(iso_env.vagrantfile.config.vm.provisioners.first.type).to eq(:puppet) 84 | end 85 | 86 | it "creates the default files" do 87 | subject.execute 88 | expect(Dir.entries(iso_env.cwd)).to include("puppet") 89 | expect(Dir.entries(File.join(iso_env.cwd, "puppet"))).to include("manifests") 90 | expect(Dir.entries(File.join(iso_env.cwd, "puppet", "manifests"))).to include("default.pp") 91 | end 92 | end 93 | 94 | describe "shorthand" do 95 | let(:argv) { ["--puppet"] } 96 | it "creates a vagrantfile with a puppet provisioner" do 97 | subject.execute 98 | expect(iso_env.vagrantfile.config.vm.provisioners.first.type).to eq(:puppet) 99 | end 100 | end 101 | 102 | describe "librarian puppet" do 103 | let(:argv) { ["--puppet", "--puppet-librarian-puppet"] } 104 | it "is passed into the Vagrantfile" do 105 | subject.execute 106 | expect(iso_env.vagrantfile.config.librarian_puppet.placeholder_filename).to eq(".gitignore") 107 | end 108 | 109 | it "creates the modules directory and placeholder" do 110 | subject.execute 111 | expect(Dir.entries(File.join(iso_env.cwd, "puppet"))).to include("modules") 112 | expect(Dir.entries(File.join(iso_env.cwd, "puppet", "modules"))).to include(".gitignore") 113 | end 114 | 115 | it "creates the Puppetfile" do 116 | subject.execute 117 | expect(Dir.entries(File.join(iso_env.cwd, "puppet"))).to include("Puppetfile") 118 | end 119 | 120 | it "contains the plugin" do 121 | subject.execute 122 | pluginsfile = File.readlines(File.join(iso_env.cwd, ".vagrantplugins")).join 123 | expect(pluginsfile).to include("vagrant-librarian-puppet") 124 | end 125 | 126 | describe "negative" do 127 | let(:argv) { ["--puppet", "--no-puppet-librarian-puppet"] } 128 | it "shouldn't be included" do 129 | subject.execute 130 | # This should be the default, as long as the plugin is installed 131 | expect(iso_env.vagrantfile.config.librarian_puppet.placeholder_filename).to eq(".PLACEHOLDER") 132 | end 133 | end 134 | end 135 | 136 | describe "hiera" do 137 | let(:argv) { ["--puppet", "--puppet-hiera"] } 138 | it "is passed into the Vagrantfile" do 139 | subject.execute 140 | expect(iso_env.vagrantfile.config.vm.provisioners.first.config.hiera_config_path).to eq("puppet/hiera.yaml") 141 | end 142 | 143 | it "creates the file" do 144 | subject.execute 145 | expect(Dir.entries(File.join(iso_env.cwd, "puppet"))).to include("hiera.yaml") 146 | expect(Dir.entries(File.join(iso_env.cwd, "puppet"))).to include("hieradata") 147 | expect(Dir.entries(File.join(iso_env.cwd, "puppet", "hieradata"))).to include("common.yaml") 148 | end 149 | 150 | describe "hiera.yaml" do 151 | it "declares a datadir contains a common.yaml file" do 152 | subject.execute 153 | hiera_obj = YAML.load(File.read(File.join(iso_env.cwd, "puppet", "hiera.yaml"))) 154 | datadir = hiera_obj[:yaml][:datadir] 155 | expect(datadir).to start_with("/vagrant") 156 | datadir_path = File.join(iso_env.cwd, datadir.sub("/vagrant/", "")) 157 | expect(datadir_path).to satisfy { |path| Dir.exist?(path) } 158 | expect(Dir.entries(datadir_path)).to include("common.yaml") 159 | end 160 | end 161 | end 162 | end 163 | 164 | context "winrm" do 165 | describe "basic" do 166 | let(:argv) { ["--winrm"] } 167 | it "creates a vagrantfile with the winrm communicator" do 168 | subject.execute 169 | expect(iso_env.vagrantfile.config.vm.communicator).to eq(:winrm) 170 | expect(iso_env.vagrantfile.config.winrm.username).to eq(described_class::DEFAULT_WINRM_USERNAME) 171 | expect(iso_env.vagrantfile.config.winrm.password).to eq(described_class::DEFAULT_WINRM_PASSWORD) 172 | expect(iso_env.vagrantfile.config.winrm.transport).to eq(:plaintext) 173 | end 174 | end 175 | 176 | describe "sspinegotiate" do 177 | let(:argv) { ["--winrm", "--servers", "server1"] } 178 | it "creates a vagrantfile with the winrm communicator" do 179 | subject.execute 180 | config = iso_env.vagrantfile.machine_config(:server1, :managed, nil)[:config] 181 | expect(config.winrm.transport).to eq(:sspinegotiate) 182 | end 183 | end 184 | 185 | describe "winrms" do 186 | let(:argv) { ["--winrm"] } 187 | it "includes the vagrant-winrm-s plugin"do 188 | subject.execute 189 | pluginsfile = File.readlines(File.join(iso_env.cwd, ".vagrantplugins")).join 190 | expect(pluginsfile).to include("vagrant-winrm-s") 191 | end 192 | end 193 | 194 | describe "username" do 195 | let(:argv) { ["--winrm", "--winrm-username", "WINRM_USERNAME"] } 196 | it "is parsed correctly" do 197 | subject.execute 198 | expect(iso_env.vagrantfile.config.winrm.username).to eq("WINRM_USERNAME") 199 | end 200 | end 201 | 202 | describe "password" do 203 | let(:argv) { ["--winrm", "--winrm-password", "WINRM_PASSWORD"] } 204 | it "is parsed correctly" do 205 | subject.execute 206 | expect(iso_env.vagrantfile.config.winrm.password).to eq("WINRM_PASSWORD") 207 | end 208 | end 209 | end 210 | 211 | context "ssh" do 212 | describe "default" do 213 | it "has default username and password" do 214 | subject.execute 215 | expect(iso_env.vagrantfile.config.ssh.username).to eq(described_class::DEFAULT_SSH_USERNAME) 216 | private_key_path = iso_env.vagrantfile.config.ssh.private_key_path.first 217 | expect(private_key_path).to eq(described_class::DEFAULT_SSH_PRIVATE_KEY_PATH) 218 | end 219 | end 220 | 221 | describe "username" do 222 | let(:argv) { ["--ssh-username", "SSH_USERNAME"] } 223 | it "is passed through" do 224 | subject.execute 225 | expect(iso_env.vagrantfile.config.ssh.username).to eq("SSH_USERNAME") 226 | end 227 | end 228 | 229 | describe "password" do 230 | let(:argv) { ["--ssh-password", "SSH_PASSWORD"] } 231 | it "is passed through" do 232 | subject.execute 233 | expect(iso_env.vagrantfile.config.ssh.password).to eq("SSH_PASSWORD") 234 | end 235 | end 236 | 237 | describe "private key path" do 238 | let(:argv) { ["--ssh-private-key-path", "SSH_PRIVATE_KEY_PATH"] } 239 | it "is passed through" do 240 | subject.execute 241 | expect(iso_env.vagrantfile.config.ssh.private_key_path.first).to eq("SSH_PRIVATE_KEY_PATH") 242 | expect(iso_env.vagrantfile.config.ssh.password).to be_nil 243 | end 244 | end 245 | end 246 | 247 | context "plugins" do 248 | describe "default" do 249 | it "has default plugins in .vagrantplugins" do 250 | subject.execute 251 | # Since the plugin stuff isn't part of the actual Vagrantfile spec, we'll 252 | # just peek at the text of the file 253 | pluginsfile = File.readlines(File.join(iso_env.cwd, ".vagrantplugins")).join 254 | described_class::DEFAULT_PLUGINS.each do |plugin| 255 | expect(pluginsfile).to include("required_plugins[\"#{plugin}\"]") 256 | end 257 | end 258 | end 259 | 260 | describe "specified plugins" do 261 | let(:argv) { ["--plugins", "plugin1,plugin2"] } 262 | it "are required" do 263 | subject.execute 264 | pluginsfile = File.readlines(File.join(iso_env.cwd, ".vagrantplugins")).join 265 | expect(pluginsfile).to include("required_plugins[\"plugin1\"]") 266 | expect(pluginsfile).to include("required_plugins[\"plugin2\"]") 267 | end 268 | end 269 | end 270 | 271 | context "servers" do 272 | describe "default" do 273 | it "has no servers in vagrantfile" do 274 | subject.execute 275 | vagrantfile = File.readlines(File.join(iso_env.cwd, "Vagrantfile")).join 276 | expect(vagrantfile).to include("managed_servers = %w( )") 277 | end 278 | end 279 | 280 | describe "specified servers" do 281 | let(:argv) { ["--servers", "server1,server2"] } 282 | it "are required" do 283 | subject.execute 284 | vagrantfile = File.readlines(File.join(iso_env.cwd, "Vagrantfile")).join 285 | expect(vagrantfile).to include("managed_servers = %w( server1 server2 )") 286 | expect(iso_env.vagrantfile.machine_config(:server1, :managed, nil)).to_not be_nil 287 | expect(iso_env.vagrantfile.machine_config(:server2, :managed, nil)).to_not be_nil 288 | end 289 | end 290 | end 291 | 292 | context "environments" do 293 | let(:argv) { ["--environments", "a,b,c"] } 294 | describe "vagrantfile" do 295 | it "should contain the loading code" do 296 | subject.execute 297 | vagrantfile = File.readlines(File.join(iso_env.cwd, "Vagrantfile")).join 298 | expect(vagrantfile).to include("managed_servers = VagrantPlugins::Orchestrate::Plugin.load_servers_for_branch") 299 | end 300 | end 301 | 302 | describe "servers.json" do 303 | it "should exist in the target directory" do 304 | subject.execute 305 | expect(Dir.entries(iso_env.cwd)).to include("servers.json") 306 | end 307 | end 308 | end 309 | 310 | context "git" do 311 | describe ".gitignore" do 312 | it "is written" do 313 | subject.execute 314 | expect(Dir.entries(iso_env.cwd)).to include(".gitignore") 315 | end 316 | 317 | it "contains ignored paths" do 318 | subject.execute 319 | contents = File.readlines(File.join(iso_env.cwd, ".gitignore")).join 320 | expect(contents).to include(".vagrant/") 321 | end 322 | end 323 | end 324 | 325 | context "box" do 326 | describe "dummy.box" do 327 | it "winds up in the target directory" do 328 | subject.execute 329 | expect(Dir.entries(iso_env.cwd)).to include("dummy.box") 330 | end 331 | end 332 | end 333 | 334 | context "orchestrate config" do 335 | describe "filter_managed_servers" do 336 | it "is set to true" do 337 | subject.execute 338 | expect(iso_env.vagrantfile.config.orchestrate.filter_managed_commands).to be true 339 | end 340 | end 341 | end 342 | 343 | context "deployment tracker" do 344 | describe "deployment tracker host is specified" do 345 | let(:argv) { ["--deployment-tracker-host", "http://deploymenttracker.io"] } 346 | it "should be set in the Vagrantfile" do 347 | subject.execute 348 | expect(iso_env.vagrantfile.config.orchestrate.tracker_host).to eq("http://deploymenttracker.io") 349 | end 350 | end 351 | end 352 | end 353 | -------------------------------------------------------------------------------- /spec/vagrant-orchestrate/command/root_spec.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-orchestrate/command/root" 2 | require "vagrant-orchestrate/command/init" 3 | require "vagrant-spec/unit" 4 | 5 | describe VagrantPlugins::Orchestrate::Command::Root do 6 | include_context "vagrant-unit" 7 | 8 | let(:argv) { [] } 9 | let(:iso_env) do 10 | env = isolated_environment 11 | env.vagrantfile("") 12 | env.create_vagrant_env ui_class: Vagrant::UI::Basic 13 | end 14 | 15 | subject { described_class.new(argv, iso_env) } 16 | 17 | ["", "-h", "--help"].each do |arg| 18 | describe "root help message #{arg}" do 19 | let(:argv) { [arg] } 20 | it "shows help" do 21 | output = capture_stdout { subject.execute } 22 | expect(output).to \ 23 | include("Usage: vagrant orchestrate []") 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /templates/environment/servers.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "environments": { 3 | <% environments.each_with_index do |environment, index| -%> 4 | "<%= environment %>": { 5 | "servers": [ 6 | 7 | ] 8 | }<% if index < environments.length - 1 -%>,<% end -%> 9 | <% end -%> 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /templates/puppet/Puppetfile.erb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #^syntax detection 3 | 4 | forge "https://forgeapi.puppetlabs.com" 5 | 6 | # A module from the Puppet Forge 7 | # mod 'puppetlabs/stdlib' 8 | -------------------------------------------------------------------------------- /templates/puppet/hiera.yaml.erb: -------------------------------------------------------------------------------- 1 | --- 2 | :backends: 3 | - yaml 4 | 5 | :hierarchy: 6 | # - "node/%{::fqdn}" 7 | - "common" 8 | 9 | :yaml: 10 | :datadir: '/vagrant/puppet/hieradata' 11 | -------------------------------------------------------------------------------- /templates/puppet/hiera/common.yaml.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimpress-MCP/vagrant-orchestrate/16487bbcbc49846b4b23d1a542c2231f1e96d194/templates/puppet/hiera/common.yaml.erb -------------------------------------------------------------------------------- /templates/vagrant/.vagrantplugins.erb: -------------------------------------------------------------------------------- 1 | required_plugins = {} 2 | # Example usage: 3 | # required_plugins["plugin-name"] = { version: "1.2.3", source: "https://rubygems.org" } 4 | <% plugins.each do |p| -%> 5 | required_plugins["<%= p %>"] = {} 6 | <% end -%> 7 | 8 | needs_restart = false 9 | required_plugins.each do |plugin, options| 10 | version = options[:version] 11 | 12 | unless Vagrant.has_plugin?(plugin, version) 13 | command = "vagrant plugin install #{plugin}" 14 | command += " --plugin-version #{version}" if version 15 | command += " --plugin-source #{options[:source]}" if options[:source] 16 | system command 17 | needs_restart = true 18 | end 19 | end 20 | 21 | if needs_restart 22 | exec "vagrant #{ARGV.join' '}" 23 | end 24 | -------------------------------------------------------------------------------- /templates/vagrant/Vagrantfile.erb: -------------------------------------------------------------------------------- 1 | <% if environments.any? -%> 2 | managed_servers = VagrantPlugins::Orchestrate::Plugin.load_servers_for_branch 3 | <% else -%> 4 | managed_servers = %w( <% servers.each do |s| -%><%= s %> <% end -%>) 5 | <% end -%> 6 | 7 | Vagrant.configure("2") do |config| 8 | # This disables up, provision, reload, and destroy for managed servers. Use 9 | # `vagrant orchestrate push` to communicate with managed servers. 10 | config.orchestrate.filter_managed_commands = true 11 | <% if creds_prompt -%> 12 | config.orchestrate.credentials.prompt = true 13 | <% end -%> 14 | <% if creds_file_path -%> 15 | config.orchestrate.credentials.file_path = "<%= creds_file_path%>" 16 | <% end -%> 17 | <% if tracker_host -%> 18 | config.orchestrate.tracker_host = "<%= tracker_host%>" 19 | <% end -%> 20 | 21 | <% if provisioners.include? "shell" -%> 22 | <% shell_paths.each do |path| -%> 23 | config.vm.provision "shell", path: "<%= path %>" 24 | <% end -%> 25 | <% if shell_inline -%> 26 | config.vm.provision "shell", inline: "<%= shell_inline %>" 27 | <% end -%> 28 | <% end -%> 29 | <% if provisioners.include? "puppet" -%> 30 | <% if puppet_librarian_puppet -%> 31 | config.librarian_puppet.placeholder_filename = ".gitignore" 32 | <% end -%> 33 | config.vm.provision "puppet" do |puppet| 34 | <% if puppet_librarian_puppet -%> 35 | puppet.module_path = 'puppet/modules' 36 | <% end -%> 37 | <% if puppet_hiera -%> 38 | puppet.hiera_config_path = 'puppet/hiera.yaml' 39 | <% end -%> 40 | puppet.manifests_path = 'puppet/manifests' 41 | end 42 | <% end -%> 43 | <% if communicator == "ssh" -%> 44 | config.ssh.username = "<%= ssh_username %>" 45 | <% if ssh_password -%> 46 | config.ssh.password = "<%= ssh_password %>" 47 | <% end -%> 48 | <% if ssh_private_key_path -%> 49 | config.ssh.private_key_path = "<%= ssh_private_key_path %>" 50 | <% end -%> 51 | <% end -%> 52 | <% if communicator == "winrm" -%> 53 | config.vm.communicator = "<%= communicator %>" 54 | config.vm.guest = :windows 55 | config.winrm.username = "<%= winrm_username %>" 56 | config.winrm.password = "<%= winrm_password %>" 57 | <% end -%> 58 | 59 | config.vm.define "local", primary: true do |local| 60 | local.vm.box = "ubuntu/trusty64" 61 | end 62 | 63 | managed_servers.each do |instance| 64 | config.vm.define instance, autostart: false do |box| 65 | box.vm.box = "managed-server-dummy" 66 | box.vm.box_url = "./dummy.box" 67 | box.vm.provider :managed do |provider| 68 | provider.server = instance 69 | end 70 | <% if communicator == "winrm" =%> 71 | box.winrm.transport = :sspinegotiate 72 | <% end -%> 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /templates/vagrant/dummy.box: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimpress-MCP/vagrant-orchestrate/16487bbcbc49846b4b23d1a542c2231f1e96d194/templates/vagrant/dummy.box -------------------------------------------------------------------------------- /vagrant-orchestrate.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "vagrant-orchestrate/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "vagrant-orchestrate" 8 | spec.version = VagrantPlugins::Orchestrate::VERSION 9 | spec.authors = ["Christopher Baldauf"] 10 | spec.email = ["cbaldauf@cimpress.com"] 11 | spec.summary = "Vagrant plugin to orchestrate the deployment of managed servers." 12 | spec.homepage = "https://github.com/Cimpress-MCP/vagrant-orchestrate" 13 | spec.license = "Apache 2.0" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_runtime_dependency "deployment-tracker-client", "~> 0.1" 21 | 22 | spec.add_development_dependency "bundler", "~> 1.6" 23 | spec.add_development_dependency "rake", "~> 10.0" 24 | spec.add_development_dependency "rspec" 25 | spec.add_development_dependency "rubocop", "~> 0.28" 26 | # See Gemfile for additional development dependencies that were not available 27 | # on rubygems (or another gem source), but needed to be downloaded from git. 28 | end 29 | -------------------------------------------------------------------------------- /vagrant-spec.config.rb: -------------------------------------------------------------------------------- 1 | Vagrant::Spec::Acceptance.configure do |c| 2 | c.component_paths << File.join("acceptance") 3 | c.skeleton_paths << File.join("acceptance", "support-skeletons") 4 | end 5 | --------------------------------------------------------------------------------