├── .rspec ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── TODO.md ├── Vagrantfile ├── lib ├── vagabond.rb └── vagabond │ ├── matchers.rb │ ├── matchers │ ├── file.rb │ ├── gem.rb │ ├── package.rb │ └── service.rb │ ├── resources.rb │ └── resources │ ├── file.rb │ ├── gem.rb │ ├── package.rb │ └── service.rb └── spec ├── cookbooks └── vagabond │ ├── metadata.rb │ └── recipes │ └── default.rb ├── file_spec.rb ├── gem_spec.rb ├── package_spec.rb └── service_spec.rb /.rspec: -------------------------------------------------------------------------------- 1 | --color --format documentation 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'rake' 4 | gem 'vagrant' 5 | 6 | group :test do 7 | gem 'rspec' 8 | gem 'pry' 9 | end 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | archive-tar-minitar (0.5.2) 5 | coderay (0.9.8) 6 | diff-lcs (1.1.3) 7 | erubis (2.7.0) 8 | ffi (1.0.9) 9 | i18n (0.5.0) 10 | json (1.5.4) 11 | method_source (0.6.5) 12 | ruby_parser (>= 2.0.5) 13 | net-scp (1.0.4) 14 | net-ssh (>= 1.99.1) 15 | net-ssh (2.1.4) 16 | pry (0.9.3) 17 | coderay (>= 0.9.8) 18 | method_source (>= 0.6.0) 19 | ruby_parser (>= 2.0.5) 20 | slop (~> 1.9.0) 21 | rake (0.9.2) 22 | rspec (2.6.0) 23 | rspec-core (~> 2.6.0) 24 | rspec-expectations (~> 2.6.0) 25 | rspec-mocks (~> 2.6.0) 26 | rspec-core (2.6.4) 27 | rspec-expectations (2.6.0) 28 | diff-lcs (~> 1.1.2) 29 | rspec-mocks (2.6.0) 30 | ruby_parser (2.2.0) 31 | sexp_processor (~> 3.0) 32 | sexp_processor (3.0.6) 33 | slop (1.9.1) 34 | thor (0.14.6) 35 | vagrant (0.8.6) 36 | archive-tar-minitar (= 0.5.2) 37 | erubis (~> 2.7.0) 38 | i18n (~> 0.5.0) 39 | json (~> 1.5.1) 40 | net-scp (~> 1.0.4) 41 | net-ssh (~> 2.1.4) 42 | thor (~> 0.14.6) 43 | virtualbox (~> 0.9.1) 44 | virtualbox (0.9.2) 45 | ffi (~> 1.0.9) 46 | 47 | PLATFORMS 48 | ruby 49 | 50 | DEPENDENCIES 51 | pry 52 | rake 53 | rspec 54 | vagrant 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vagabond 2 | 3 | Vagabond is currently a work-in-progress. Features may be missing or subject to change. 4 | 5 | ## What it is 6 | 7 | Vagabond — until the 0.1.0 release — is a playground for a few different approaches for integration testing of infrastructure. We're still determining how best to handle a lot of things with the goal of making Vagabond very modular so we can re-use parts of it for other, similar projects (think Vagabond but on EC2 / Rackspace). 8 | 9 | When we started on this particular revision of the code we pretty much threw out the old code because it made some fundamentally wrong assumptions and wouldn't have worked going forward, so all of this is new. 10 | 11 | ## What it isn't 12 | 13 | Stable (yet). 14 | 15 | Finished (yet). 16 | 17 | ## That Said... 18 | 19 | We'd love some of your help or input! 20 | 21 | ## Developing 22 | 23 | 1. Install VirtualBox. 24 | 25 | 2. Install Bundler. 26 | 27 | 3. Run `bundle install` 28 | 29 | 4. Run `rake vagabond:spec` -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'vagrant' 3 | 4 | 5 | namespace :vagabond do 6 | task :full_spec => [:cleanup, :spec] 7 | 8 | desc "Remove existing vagrant vm" 9 | task :cleanup do 10 | env = Vagrant::Environment.new 11 | puts "vagrant destroy" 12 | env.cli("destroy") 13 | end 14 | 15 | desc "Run specs on vagabond" 16 | task :spec do 17 | env = Vagrant::Environment.new 18 | puts "vagrant up" 19 | env.cli("up") 20 | puts "vagrant provision" 21 | env.cli("provision") 22 | env.vms.each do | name, vm | 23 | vm.ssh.execute do | ssh | 24 | puts "rspec" 25 | puts ssh.exec!('cd /vagrant && rspec spec/*_spec.rb') 26 | end 27 | end 28 | end 29 | end 30 | 31 | require 'rspec/core' 32 | require 'rspec/core/rake_task' 33 | RSpec::Core::RakeTask.new(:spec) do |spec| 34 | spec.pattern = FileList['spec/**/*_spec.rb'] 35 | end 36 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Matchers and Resources 2 | 3 | * file (partially implemented) 4 | - needs has_content(Regexp) matcher 5 | * package (partially implemented) 6 | - rpm support? 7 | - merge gem fu into package as well? 8 | * service (partially implemented) 9 | * user (needs implemented) 10 | * group (needs implemented) 11 | * cron (needs implemented) 12 | * exec (needs implemented) 13 | - needs some method of logging output of all execs run somewhere... parse logs for chef/puppet runs? 14 | - needs ordering support (puppet dependency graph) 15 | * mount (needs implemented) 16 | - is a fs mounted or not? 17 | - is it mounted in the right location? 18 | - are the mount options right? 19 | - is the mounted fs right? 20 | 21 | # Gemification 22 | 23 | * Jeweler for this, probably 24 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant::Config.run do |config| 2 | # All Vagrant configuration is done here. The most common configuration 3 | # options are documented and commented below. For a complete reference, 4 | # please see the online documentation at vagrantup.com. 5 | 6 | # Every Vagrant virtual environment requires a box to build off of. 7 | config.vm.box = "lucid64" 8 | 9 | # The url from where the 'config.vm.box' box will be fetched if it 10 | # doesn't already exist on the user's system. 11 | config.vm.box_url = "http://files.vagrantup.com/lucid64.box" 12 | 13 | # Boot with a GUI so you can see the screen. (Default is headless) 14 | # config.vm.boot_mode = :gui 15 | 16 | # Assign this VM to a host only network IP, allowing you to access it 17 | # via the IP. 18 | config.vm.network "10.1.1.2" 19 | 20 | # Forward a port from the guest to the host, which allows for outside 21 | # computers to access the VM, whereas host only networking does not. 22 | # config.vm.forward_port "http", 80, 8080 23 | 24 | # Share an additional folder to the guest VM. The first argument is 25 | # an identifier, the second is the path on the guest to mount the 26 | # folder, and the third is the path on the host to the actual folder. 27 | # config.vm.share_folder "v-data", "/vagabond", "." 28 | 29 | # Enable provisioning with Puppet stand alone. Puppet manifests 30 | # are contained in a directory path relative to this Vagrantfile. 31 | # You will need to create the manifests directory and a manifest in 32 | # the file base.pp in the manifests_path directory. 33 | # 34 | # An example Puppet manifest to provision the message of the day: 35 | # 36 | # # group { "puppet": 37 | # # ensure => "present", 38 | # # } 39 | # # 40 | # # File { owner => 0, group => 0, mode => 0644 } 41 | # # 42 | # # file { '/etc/motd': 43 | # # content => "Welcome to your Vagrant-built virtual machine! 44 | # # Managed by Puppet.\n" 45 | # # } 46 | # 47 | # config.vm.provision :puppet do |puppet| 48 | # puppet.manifests_path = "manifests" 49 | # puppet.manifest_file = "base.pp" 50 | # end 51 | 52 | # Enable provisioning with chef solo, specifying a cookbooks path (relative 53 | # to this Vagrantfile), and adding some recipes and/or roles. 54 | # 55 | config.vm.provision :chef_solo do |chef| 56 | chef.cookbooks_path = "spec/cookbooks" 57 | chef.add_recipe "vagabond" 58 | end 59 | 60 | # Enable provisioning with chef server, specifying the chef server URL, 61 | # and the path to the validation key (relative to this Vagrantfile). 62 | # 63 | # The Opscode Platform uses HTTPS. Substitute your organization for 64 | # ORGNAME in the URL and validation key. 65 | # 66 | # If you have your own Chef Server, use the appropriate URL, which may be 67 | # HTTP instead of HTTPS depending on your configuration. Also change the 68 | # validation key to validation.pem. 69 | # 70 | # config.vm.provision :chef_client do |chef| 71 | # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" 72 | # chef.validation_key_path = "ORGNAME-validator.pem" 73 | # end 74 | # 75 | # If you're using the Opscode platform, your validator client is 76 | # ORGNAME-validator, replacing ORGNAME with your organization name. 77 | # 78 | # IF you have your own Chef Server, the default validation client name is 79 | # chef-validator, unless you changed the configuration. 80 | # 81 | # chef.validation_client_name = "ORGNAME-validator" 82 | end 83 | -------------------------------------------------------------------------------- /lib/vagabond.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'vagabond')) 2 | require 'vagabond/matchers' 3 | require 'vagabond/resources' 4 | 5 | module Vagabond 6 | include Vagabond::Matchers 7 | include Vagabond::Resources 8 | 9 | VERSION = "0.0.1" 10 | end 11 | -------------------------------------------------------------------------------- /lib/vagabond/matchers.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'matchers')) 2 | 3 | require 'vagabond/matchers/file' 4 | require 'vagabond/matchers/gem' 5 | require 'vagabond/matchers/package' 6 | require 'vagabond/matchers/service' 7 | 8 | module Vagabond 9 | module Matchers 10 | include Vagabond::Matchers::File 11 | include Vagabond::Matchers::Gem 12 | include Vagabond::Matchers::Package 13 | include Vagabond::Matchers::Service 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/vagabond/matchers/file.rb: -------------------------------------------------------------------------------- 1 | require 'etc' 2 | 3 | module Vagabond 4 | module Matchers 5 | module File 6 | RSpec::Matchers.define :exist do 7 | match do |actual| 8 | ::File.exist? actual.title 9 | end 10 | 11 | failure_message_for_should do |actual| 12 | "expected #{actual.to_s} would exist" 13 | end 14 | 15 | failure_message_for_should_not do |actual| 16 | "expected #{actual.to_s} would not exist" 17 | end 18 | end 19 | 20 | RSpec::Matchers.define :have_owner do |expected| 21 | match do |actual| 22 | ::Etc.getpwuid(::File.stat(actual.title).uid).name == expected 23 | end 24 | end 25 | 26 | RSpec::Matchers.define :have_group do |expected| 27 | match do |actual| 28 | ::Etc.getgrgid(::File.stat(actual.title).gid).name == expected 29 | end 30 | end 31 | 32 | RSpec::Matchers.define :have_file_type do |expected| 33 | match do |actual| 34 | ::File.ftype(actual.title) == expected.to_s 35 | end 36 | 37 | description do 38 | "should be a #{expected}" 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/vagabond/matchers/gem.rb: -------------------------------------------------------------------------------- 1 | module Vagabond 2 | module Matchers 3 | module Gem 4 | RSpec::Matchers.define :be_installed do 5 | match do |actual| 6 | actual.state =~ /^installed$/ 7 | end 8 | 9 | failure_message_for_should do |actual| 10 | "expected #{actual.to_s} to be installed" 11 | end 12 | 13 | failure_message_for_should_not do |actual| 14 | "expected #{actual.to_s} to not be installed" 15 | end 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/vagabond/matchers/package.rb: -------------------------------------------------------------------------------- 1 | module Vagabond 2 | module Matchers 3 | module Package 4 | RSpec::Matchers.define :be_installed do 5 | match do |actual| 6 | actual.state && actual.state =~ /^installed$/ 7 | end 8 | 9 | failure_message_for_should do |actual| 10 | "expected #{actual.to_s} to be installed" 11 | end 12 | 13 | failure_message_for_should_not do |actual| 14 | "expected #{actual.to_s} to not be installed" 15 | end 16 | end 17 | 18 | RSpec::Matchers.define :be_version do |expected| 19 | match do |actual| 20 | actual.version == expected 21 | end 22 | 23 | failure_message_for_should do |actual| 24 | "expected #{actual.to_s} to be version '#{expected}'" 25 | end 26 | 27 | failure_message_for_should_not do |actual| 28 | "expected #{actual.to_s} to not be version '#{expected}'" 29 | end 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/vagabond/matchers/service.rb: -------------------------------------------------------------------------------- 1 | module Vagabond 2 | module Matchers 3 | module Service 4 | RSpec::Matchers.define :be_running do 5 | match do |actual| 6 | @status = `service #{actual.name} status 2>&1`.chomp 7 | $? == 0 8 | end 9 | 10 | failure_message_for_should do |actual| 11 | "expected #{actual.name} to be running, but wasn't: #{@status}" 12 | end 13 | end 14 | end 15 | end 16 | end 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/vagabond/resources.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'resources')) 2 | 3 | require 'vagabond/resources/file' 4 | require 'vagabond/resources/gem' 5 | require 'vagabond/resources/package' 6 | require 'vagabond/resources/service' 7 | 8 | module Vagabond 9 | module Resources 10 | def file(name, options = {}) 11 | Vagabond::Resources::File.new(name, options) 12 | end 13 | 14 | def gem(name, options = {}) 15 | Vagabond::Resources::Gem.new(name, options) 16 | end 17 | 18 | def package(name, options = {}) 19 | Vagabond::Resources::Package.new(name, options) 20 | end 21 | 22 | def service(name) 23 | Vagabond::Resources::Service.new(name) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/vagabond/resources/file.rb: -------------------------------------------------------------------------------- 1 | module Vagabond 2 | module Resources 3 | class File 4 | attr_accessor :name, :owner, :group, :path 5 | 6 | def initialize(name, options = {}) 7 | @name = name 8 | if options[:owner] 9 | @owner = options[:owner] 10 | end 11 | if options[:group] 12 | @group = options[:group] 13 | end 14 | if options[:path] 15 | @path = options[:path] 16 | end 17 | end 18 | 19 | def title 20 | @path || @name 21 | end 22 | 23 | def to_s 24 | "file(#{title})" 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/vagabond/resources/gem.rb: -------------------------------------------------------------------------------- 1 | module Vagabond 2 | module Resources 3 | class Gem 4 | attr_accessor :name 5 | 6 | def initialize(name, options = {}) 7 | @name = name 8 | end 9 | 10 | def title 11 | @name 12 | end 13 | 14 | def to_s 15 | "gem(#{title})" 16 | end 17 | 18 | def state 19 | `gem list -i #{@name}` 20 | if $? == 0 21 | "installed" 22 | else 23 | "not installed" 24 | end 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/vagabond/resources/package.rb: -------------------------------------------------------------------------------- 1 | module Vagabond 2 | module Resources 3 | class Package 4 | attr_accessor :name 5 | 6 | def initialize(name, options = {}) 7 | @name = name 8 | end 9 | 10 | def title 11 | @name 12 | end 13 | 14 | def to_s 15 | "package(#{title})" 16 | end 17 | 18 | def state 19 | aptitude.match(/^State: (.*)$/)[1] rescue nil 20 | end 21 | 22 | def version 23 | aptitude.match(/^Version: (.*)$/)[1] rescue nil 24 | end 25 | 26 | private 27 | def aptitude 28 | results = `sudo aptitude show #{@name}` 29 | if $? == 0 30 | results 31 | else 32 | "" 33 | end 34 | end 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /lib/vagabond/resources/service.rb: -------------------------------------------------------------------------------- 1 | module Vagabond 2 | module Resources 3 | class Service 4 | attr_accessor :name 5 | 6 | def initialize(name) 7 | @name = name 8 | end 9 | 10 | 11 | def to_s 12 | "service(#{name})" 13 | end 14 | 15 | end 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /spec/cookbooks/vagabond/metadata.rb: -------------------------------------------------------------------------------- 1 | maintainer "Will Farrington" 2 | maintainer_email "will@railsmachine.com" 3 | license "MIT" 4 | recipe "vagabond", "For testing vagabond" 5 | 6 | supports "ubuntu" -------------------------------------------------------------------------------- /spec/cookbooks/vagabond/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # Necessary steps to run specs 2 | 3 | gem_package 'bundler' do 4 | action :install 5 | end 6 | 7 | gem_package 'rspec' do 8 | action :install 9 | end 10 | 11 | execute "bundle install" do 12 | cwd "/vagrant" 13 | action :run 14 | end 15 | 16 | # Items for testing specs 17 | 18 | file '/etc/motd' do 19 | content 'foobar' 20 | action [:delete, :create] 21 | end 22 | 23 | execute "apt-get update" do 24 | action :run 25 | end 26 | 27 | package 'apache2-mpm-worker' do 28 | action :install 29 | end 30 | -------------------------------------------------------------------------------- /spec/file_spec.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'rspec' 3 | require 'vagabond' 4 | 5 | include Vagabond 6 | 7 | describe file("/etc/motd") do 8 | it { should exist } 9 | it { should have_owner('root') } 10 | it { should have_group('root') } 11 | it { should have_file_type(:file) } 12 | end 13 | 14 | describe file("/etc") do 15 | it { should exist } 16 | it { should have_file_type(:directory) } 17 | end -------------------------------------------------------------------------------- /spec/gem_spec.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'rspec' 3 | require 'vagabond' 4 | require 'pry' 5 | 6 | include Vagabond 7 | 8 | describe gem("twitter") do 9 | it { should_not be_installed } 10 | end 11 | 12 | describe gem("json") do 13 | it { should be_installed } 14 | end -------------------------------------------------------------------------------- /spec/package_spec.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'rspec' 3 | require 'vagabond' 4 | 5 | include Vagabond 6 | 7 | describe package("tar") do 8 | it { should be_installed } 9 | it { should be_version('1.22-2ubuntu1') } 10 | end 11 | 12 | describe package("nginx") do 13 | it { should_not be_installed } 14 | end -------------------------------------------------------------------------------- /spec/service_spec.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'rspec' 3 | require 'vagabond' 4 | require 'pry' 5 | 6 | include Vagabond 7 | 8 | describe service('stupid') do 9 | it { should_not be_running } 10 | end 11 | 12 | describe service('apache2') do 13 | it { should be_running } 14 | end 15 | 16 | 17 | --------------------------------------------------------------------------------