vm_provisioner
A Vagrantfile helper that does very basic virtual machine provisioning.
A Vagrantfile helper that does very basic virtual machine provisioning.
vm_provisioner is a small provisioning helper for use with Vagrant. It is a simplistic version of tools such as Puppet, Chef, Ansible, etc. It was created as an exercise to learn Vagrant and to aid the (re)creation of OSX-based development environments.
Here is a fragment of a Vagrantfile showing how vm_provisioner is typically used:
...
Vagrant.configure(API_VERSION) do |vagrant_config|
vagrant_config.vm.box = BOX
vagrant_config.vm.provider(PROVIDER_NAME) do |vb|
vb.name = VM_NAME
vb.gui = true
end
with vagrant_config do
install_atom
install_iterm2
install_gpg
install_git
install_github_for_mac
install_homebrew
install_ruby '2.1.2'
install_bundler
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
bundle_install
end
reboot_vm
end
end
...
vm_provisioner installs itself each time Vagrant runs a Vagrantifile that references it. However, these are the prerequisits for using it:
$ mkdir -p ~/work/demo $ cd ~/work/demo
#
# Vagrantfile to create demo development environment.
#
# TODO: annotate
API_VERSION = "2"
BOX = "OSX109"
PROVIDER_NAME = "vmware_fusion"
PROJECT_NAME = "demo"
VM_NAME = PROJECT_NAME
PROJECT_VM_DIR = "/Users/vagrant/Documents/#{PROJECT_NAME}"
PROVISIONER_URL = "https://raw.githubusercontent.com/milewdev/vm-provisioner/v2/Provisioner.rb"
Vagrant.configure(API_VERSION) do |vagrant_config|
vagrant_config.vm.box = BOX
vagrant_config.vm.provider(PROVIDER_NAME) do |vb|
vb.name = VM_NAME
vb.gui = true
end
with vagrant_config do
install_atom
install_iterm2
install_git
install_github_for_mac
install_homebrew # needed to install ruby
install_ruby '2.1.2'
install_bundler
reboot_vm
end
end
def with(vagrant_config, &block)
require "open-uri"
File.write "Provisioner.rb", open(PROVISIONER_URL).read
require_relative "Provisioner"
Provisioner.provision(vagrant_config, &block)
File.delete "Provisioner.rb"
end
$ vagrant up --provider=vmware_fusion
Prepend path to the PATH environment variable by doing:
echo 'export PATH=path:$PATH' >> ~/.bash_profile
For example, say you have installed nodejs, installed your project, and you have run npm install to install any dependencies. Any nodejs binaries, such as grunt, are now located in node_modules/.bin. So, you can add node_modules/.bin to your path:
PROJECT_GITHUB_URL = 'https://https://github.com/...'
PROJECT_VM_DIR = '/Users/vagrant/Documents/...'
...
with vagrant_config do
install_git
install_node
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
npm_install
end
add_to_path "#{PROJECT_VM_DIR}/node_modules/.bin"
end
Now, rather than having to do:
$ node_modules/.bin/grunt ...
you can just do:
$ grunt ...
Run bundle install. bundler must be installed beforehand.
You would typically install the project sources and then run bundle install to install any project dependencies. Note that bundle install is run in the current directory so you will likely want to change to the project directory directory first:
PROJECT_GITHUB_URL = 'https://https://github.com/...'
PROJECT_VM_DIR = '/Users/vagrant/Documents/...'
...
with vagrant_config do
install_git
install_homebrew
install_ruby '2.1.2'
install_bundler
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
bundle_install
end
end
Temporarily change to directory path. For example, you might do this before running bundle_install or npm_install:
PROJECT_GITHUB_URL = 'https://https://github.com/...'
PROJECT_VM_DIR = '/Users/vagrant/Documents/...'
...
with vagrant_config do
install_git
install_homebrew
install_ruby '2.1.2'
install_bundler
install_node
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
bundle_install
npm_install
end
end
Warning: cd cannot be nested:
with vagrant_config do
cd 'somewhere' do
cd 'somewhere/else' do
...
end
# Whoops! back at ~, not 'somewhere'
end
end
Clone git_repository to path. Use this command to install project source code from a git repository onto the virtual machine:
PROJECT_GITHUB_URL = 'https://https://github.com/...' PROJECT_VM_DIR = '/Users/vagrant/Documents/...' ... with vagrant_config do install_git git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR end
Install the atom editor.
with vagrant_config do install_atom end
Install the bundler Ruby gem package manager. Ruby must be installed beforehand.
with vagrant_config do install_homebrew install_ruby '2.1.2' install_bundler end
Install the Google Chrome web browser.
with vagrant_config do install_chrome end
Install the Mozilla Firefox web browser.
with vagrant_config do install_firefox end
Install git. Also copy the file ~/.gitconfig on the host machine to /Users/vagrant/.gitconfig on the virtual machine. If the optional version parameter is not provided, version '2.0.1' is installed.
with vagrant_config do install_git '2.2.0' end
Install the GitHub for Mac git client.
with vagrant_config do install_github_for_mac end
Install the gpg encryption tools. Also copy the following files from ~/.gnupg on the host machine to /Users/vagrant/.gnupg on the virtual machine: pubring.gpg, secring.gpg, trustdb.gpg, pubring.gpg~, and random_seed.
with vagrant_config do install_gpg end
Install heroku toolbelt, the Heroku command line utility.
with vagrant_config do install_heroku_toolbelt end
Install the Homebrew OS X package manager. The OS X command line tools must be installed beforehand on the base Vagrant box. During installation, homebrew/versions and homebrew/dupes are tapped.
Homebrew is a prerequisit for other vm_provisioner tasks, such as install_ruby.
with vagrant_config do install_homebrew end
Install the iterm2 OS X terminal replacement.
with vagrant_config do install_iterm2 end
Install Node.js. If the optional version parameter is not provided, version 'v0.10.32' is installed.
with vagrant_config do install_node 'v0.10.33' end
Install the PhantomJS headless browser. Homebrew must be installed beforehand.
with vagrant_config do install_homebrew install_phantomjs end
Install the PostgreSQL database management system. If the optional version parameter is not supplied, the latest version is installed. Homebrew must be installed beforehand. During installation initdb is called, and the database is set up to start when user vagrant logs in.
with vagrant_config do install_homebrew install_postgresql '92' end
Install python (v3.4.1).
with vagrant_config do install_python3 end
Install qt. Homebrew must be installed beforehand.
with vagrant_config do install_homebrew install_qt end
Install Ruby. If the optional version parameter is not supplied, version '2.1.2' is installed. Homebrew must be installed beforehand. During installation, GCC 4.2, rbenv, and ruby-build are also installed.
with vagrant_config do install_homebrew install_ruby '2.1.5' end
Install the TextMate editor.
with vagrant_config do install_textmate end
Install virtualenv (v1.11.6), the Python environment manager.
PROJECT_VM_DIR = '/Users/vagrant/Documents/...'
PROJECT_GITHUB_URL = 'https://https://github.com/...'
...
with vagrant_config do
install_git
install_python3
install_virtualenv
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
virtualenv_create
pip_install
end
end
Run npm install. Node.js must be installed beforehand.
You would typically install the project sources and then run npm install to install any project dependencies. Note that npm install is run in the current directory so you will likely want to change to the project directory directory first:
PROJECT_GITHUB_URL = 'https://https://github.com/...'
PROJECT_VM_DIR = '/Users/vagrant/Documents/...'
...
with vagrant_config do
install_git
install_node
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
npm_install
end
end
Runs pip install --requirements.txt. Python must be installed beforehand.
You would typically install the project sources and then run pip install to install any project dependencies. Note that pip install is run in the current directory so you will likely want to change to the project directory directory first:
PROJECT_VM_DIR = '/Users/vagrant/Documents/...'
PROJECT_GITHUB_URL = 'https://https://github.com/...'
...
with vagrant_config do
install_git
install_python3
install_virtualenv
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
virtualenv_create
pip_install
end
end
Reboots the virtual machine. This may be useful on some operating systems after installing software.
with vagrant_config do install_git install_homebrew install_postgresql # ... reboot_vm end
Runs virtualenv --no-site-packages --python=which python3 env. virtualenv must be installed beforehand.
You would typically install the project sources and then run virtualenv to create a new python environment in the project directory.
PROJECT_VM_DIR = '/Users/vagrant/Documents/...'
PROJECT_GITHUB_URL = 'https://https://github.com/...'
...
with vagrant_config do
install_git
install_python3
install_virtualenv
git_clone PROJECT_GITHUB_URL, PROJECT_VM_DIR
cd PROJECT_VM_DIR do
virtualenv_create
pip_install
end
end
$ git clone https://github.com/milewdev/vm-provisioner.git ~/work/vm-provisioner
The source consists of one class, Provisioner, defined in the file Provisioner.rb. There are a large number of methods but they are very repetitive and there is not yet the incentive to break them out into smaller classes.
A Vagrantfile bootstraps vm-provisioner by downloading Provisioner.rb from github and then invoking the Provisioner.provision class method, i.e. the code entry point:
PROVISIONER_URL = "https://raw.githubusercontent.com/milewdev/vm-provisioner/v2/Provisioner.rb"
...
Vagrant.configure(API_VERSION) do |vagrant_config|
...
with vagrant_config do
...
end
end
def with(vagrant_config, &block)
require "open-uri"
File.write "Provisioner.rb", open(PROVISIONER_URL).read
require_relative "Provisioner"
Provisioner.provision(vagrant_config, &block)
File.delete "Provisioner.rb"
end
Note that the URL references branch v2 rather than master:
PROVISIONER_URL = "https:// ... /vm-provisioner/v2/Provisioner.rb"
Interface-breaking changes to vm-provisioner should be stored in github in their own branch so as not to break existing Vagrantfiles.
Although there is only the one Provisioner class, it has been divided into three parts, the first for initialization and launch (where Provisioner.provision is defined), the second containing the provisioning utilities called from a Vagrantfile, for example install_git and reboot_vm, and the last containing API methods used in the implementations of the utilities, for instance dmg_install and copy_host_file_to_vm.
Adding a product will usually involve adding an install_something method to the second part of the Provisioner class:
class Provisioner
...
def install_atom
say "Installing atom editor"
zip_install 'https://atom.io/download/mac'
end
...
end
Sometimes a little more work is required:
class Provisioner
...
def install_postgresql(version = nil)
version ||= '' # blank version defaults to latest version
say "Installing PostgreSQL"
run_script <<-"EOF"
brew install postgresql#{version}
rm -rf /usr/local/var/postgres/
initdb /usr/local/var/postgres -E UTF8
mkdir -p ~/Library/LaunchAgents
ln -sfv /usr/local/opt/postgresql92/*.plist ~/Library/LaunchAgents
EOF
end
...
end
The say method prints a header line in the terminal window where Vagrant is run from. This simply helps to delimit one provisioning step from another which can be helpful when debugging errors, slow downloads, etc.
...
--------------- Installing atom editor ---------------
[default] Running provisioner: shell...
[default] Running: inline script
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 389 100 389 0 0 396 0 --:--:-- --:--:-- --:--:-- 396
100 65.7M 100 65.7M 0 0 1122k 0 0:00:59 0:00:59 --:--:-- 1058k
[default] Running provisioner: shell...
[default] Running: inline script
--------------- Installing iTerm2 ---------------
[default] Running provisioner: shell...
[default] Running: inline script
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3111k 100 3111k 0 0 880k 0 0:00:03 0:00:03 --:--:-- 880k
[default] Running provisioner: shell...
[default] Running: inline script
...
The API Reference contains a list of methods that can help to write an installer, such as zip_install, run_script, etc. Add additional helper methods, if required, or enhance the existing ones. Note: if you introduce a change that would break existing Vagrantfiles then create a new branch in github, e.g. v3, to contain the enhancements.
Copy the file host_path from the host machine to vm_path on the virtual machine.
def install_git(version = '2.0.1')
say "Installing git and copying .gitconfig from vm host"
dmg_install "http://sourceforge.net/projects/git-osx-installer/files/git-#{version}-intel-universal-snow-leopard.dmg/download?use_mirror=autoselect"
copy_host_file_to_vm "~/.gitconfig", ".gitconfig"
end
copy_host_file_to_vm is shorthand for Vagrant's config.vm.provision 'file' command. The example above could be written as:
def install_git(version = '2.0.1')
say "Installing git and copying .gitconfig from vm host"
dmg_install "http://sourceforge.net/projects/git-osx-installer/files/git-#{version}-intel-universal-snow-leopard.dmg/download?use_mirror=autoselect"
@vagrant_config.vm.provision 'file', source: '~/.gitconfig', destination: '.gitconfig'
end
Install a dmg file located at url_of_dmg_file.
def install_chrome say 'Installing google chrome browser' dmg_install 'https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg' end
If the dmg file contains one file with the extension .pkg or .mpkg then the file is installed using installer. If the dmg file contains one directory with the extension .app then the directory is copied to /Applications.
Install a pkg file located at url_of_pkg_file.
def install_heroku_toolbelt say 'Installing Heroku Toolbelt' pkg_install 'https://toolbelt.heroku.com/download/osx' end
The pkg file is installed using installer.
Contains the name of the directory where run_script is run. This is the value of 'path' passed to cd when inside of the cd block, or /Users/vagrant otherwise.
with vagrant_config do
# @run_in_directory = '/Users/vagrant'
cd '/tmp' do
# @run_in_directory = '/tmp'
end
# @run_in_directory = '/Users/vagrant'
end
Run script_code on the virtual machine in the current directory.
def bundle_install say "Running 'bundle install'" run_script "bundle install" end
run_script is shorthand for Vagrant's config.vm.provision 'shell' command. The example above could be written as:
def bundle_install
say "Running 'bundle install'"
@vagrant_config.vm.provision :shell, privileged: false, inline: <<-"EOF"
pushd #{@run_in_directory} > /dev/null
#{script_code}
popd > /dev/null
EOF
end
Note that the current directory (@run_in_directory) is normally /User/vagrant, but it can be changed temporarily using the cd command. For example, suppose we have the following provisioning method:
def pwd puts @run_in_directory end
This Vagrantfile snippit shows the effect of the cd command on @run_in_directory:
with vagrant_config do
pwd # /Users/vagrant
cd '/tmp' do
pwd # /tmp
end
pwd # /Users/vagrant
end
Prints a header line in the terminal window where Vagrant is run from. This simply helps to delimit one provisioning step from another and is intended as an aid for debugging errors, identifying slow downloads, etc.
For example, suppose we have defined the following utility methods:
def got_here say 'got here' puts 'got here too' end def got_there say 'got there' puts 'got there too' end
And we include the following in our Vagrantfile:
with vagrant_config do got_here got_there end
Then running vagrant up results in:
$ vagrant up --provider=vmware_fusion ... --------------- got here --------------- got here too --------------- got there --------------- got there too ... $
Install a tar file located at url_of_tar_file.
def install_textmate say "Installing TextMate" tar_install 'https://api.textmate.org/downloads/release' end
The contents of the tar file are simply extracted to /Applications.
The vagrant config instance that was passed to Provisioner.provision. Here is the entire trail of the vagrant configuration, starting with the Vagrantfile:
Vagrant.configure(API_VERSION) do |vagrant_config|
with vagrant_config do
ls
end
end
def with(vagrant_config, &block)
require "open-uri"
File.write "Provisioner.rb", open(PROVISIONER_URL).read
require_relative "Provisioner"
Provisioner.provision(vagrant_config, &block)
File.delete "Provisioner.rb"
end
And inside Provisioner.rb we may have something like:
class Provisioner
def self.provision(vagrant_config, &block)
Provisioner.new(vagrant_config).send(:run, &block)
end
private
def initialize(vagrant_config)
@vagrant_config = vagrant_config
@run_in_directory = @run_in_directory_default = '.'
end
...
end
...
class Provisioner
def ls
run_script 'ls'
end
end
...
class Provisioner
def run_script(script_code)
@vagrant_config.vm.provision :shell, privileged: false, inline: <<-"EOF"
pushd #{@run_in_directory} > /dev/null
#{script_code}
popd > /dev/null
EOF
end
end
Install a zip file located at url_of_zip_file.
def install_atom say "Installing atom editor" zip_install 'https://atom.io/download/mac' end
The contents of the zip file are simply extracted to /Applications.