commit 55c9be772db32d4f49bf61af1801d2d6f14a880e Author: Fabio Rehm Date: Mon Feb 25 01:58:04 2013 -0300 Not so initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d588d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*.gem +*.rbc +.bundle +.config +coverage +InstalledFiles +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp + +# YARD artifacts +.yardoc +_yardoc +doc/ + +.vagrant +/cache diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..02032a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vagrant-1.1"] + path = vagrant-1.1 + url = git://github.com/mitchellh/vagrant.git diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..16f9cdb --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--format documentation diff --git a/.vimrc b/.vimrc new file mode 100644 index 0000000..e596bb5 --- /dev/null +++ b/.vimrc @@ -0,0 +1 @@ +set wildignore+=*/vagrant-1.1/* diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..655a84e --- /dev/null +++ b/Gemfile @@ -0,0 +1,14 @@ +source 'https://rubygems.org' + +unless ENV['USER'] == 'vagrant' + puts 'This Gemfile is meant to be used from the dev box' + exit 1 +end + +gem 'rake' +gem 'net-ssh' +gem 'rspec' +gem 'guard' +gem 'guard-rspec' +gem 'rb-inotify' +gem 'log4r' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..08135b1 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,50 @@ +GEM + remote: https://rubygems.org/ + specs: + coderay (1.0.8) + diff-lcs (1.1.3) + ffi (1.4.0) + guard (1.6.2) + listen (>= 0.6.0) + lumberjack (>= 1.0.2) + pry (>= 0.9.10) + terminal-table (>= 1.4.3) + thor (>= 0.14.6) + guard-rspec (2.4.0) + guard (>= 1.1) + rspec (~> 2.11) + listen (0.7.2) + log4r (1.1.10) + lumberjack (1.0.2) + method_source (0.8.1) + net-ssh (2.6.5) + pry (0.9.12) + coderay (~> 1.0.5) + method_source (~> 0.8) + slop (~> 3.4) + rake (10.0.3) + rb-inotify (0.8.8) + ffi (>= 0.5.0) + rspec (2.12.0) + rspec-core (~> 2.12.0) + rspec-expectations (~> 2.12.0) + rspec-mocks (~> 2.12.0) + rspec-core (2.12.2) + rspec-expectations (2.12.1) + diff-lcs (~> 1.1.3) + rspec-mocks (2.12.2) + slop (3.4.3) + terminal-table (1.4.5) + thor (0.17.0) + +PLATFORMS + ruby + +DEPENDENCIES + guard + guard-rspec + log4r + net-ssh + rake + rb-inotify + rspec diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..a148482 --- /dev/null +++ b/Guardfile @@ -0,0 +1,10 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +raise 'You should start guard from the dev box!' unless ENV['USER'] == 'vagrant' + +guard 'rspec' do + watch(%r{^spec/.+_spec\.rb$}) + watch('spec/spec_helper.rb') { 'spec' } + watch('lib/provider') { 'spec' } +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5f698a --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# vagrant-lxc + +Highly experimental Linux Containers support for Vagrant 1.1 + +## WARNING + +Please keep in mind that this is not even alpha software and things might go wrong. +Although I'm brave enough to use it on my physical machine, its recommended that you +try it out on the Vagrant dev box ;) + +## Development + +On your host: + +```terminal +./setup-vagrant-dev-box +vagrant ssh +``` + +On the guest machine: + +```terminal +mkdir /tmp/vagrant-lxc +cp /vagrant/config.yml.sample /tmp/vagrant-lxc/config.yml +cd /tmp/vagrant-lxc +/vagrant/lib/provider up +/vagrant/lib/provider ssh +``` + +## Troubleshooting + +If your container / dev box start acting weird, run `vagrant reload` to see if +things get back to normal. + +In case `vagrant reload` doesn't work, restore the VirtualBox snapshot that was +created automagically right after `./setup-vagrant-dev-box` finished by running +the same script again and selecting the `[r]estore snapshot` option when asked. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..eea5261 --- /dev/null +++ b/Rakefile @@ -0,0 +1,5 @@ +raise 'This Rakefile is meant to be used from the dev box' unless ENV['USER'] == 'vagrant' + +Dir['./tasks/**/*.rake'].each { |f| load f } + +task :default => :spec diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..6bdbdbf --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,19 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant::Config.run do |config| + config.vm.box = "quantal64" + config.vm.box_url = "https://github.com/downloads/roderik/VagrantQuantal64Box/quantal64.box" + + config.vm.network :hostonly, "192.168.33.10" + config.vm.forward_port 80, 8080 + config.vm.forward_port 2222, 2223 + + config.vm.customize [ + "modifyvm", :id, + "--memory", 1024, + "--cpus", "2" + ] + + config.vm.share_folder("v-root", "/vagrant", ".", :nfs => true) +end diff --git a/config.yml.sample b/config.yml.sample new file mode 100644 index 0000000..045a4ef --- /dev/null +++ b/config.yml.sample @@ -0,0 +1,5 @@ +--- +ip: 10.0.3.100 +forwards: +- - 2222 + - 22 diff --git a/lib/provider b/lib/provider new file mode 100755 index 0000000..3c8f9a5 --- /dev/null +++ b/lib/provider @@ -0,0 +1,277 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'log4r' +require 'yaml' +require 'shellwords' +require 'optparse' +require 'net/ssh' + +# Based on actions available to the VirtualBox provider: +# https://github.com/mitchellh/vagrant/tree/master/plugins/providers/virtualbox +class Provider + WAIT = 5 + + def initialize(config) + @config = config + @logger = Log4r::Logger.new("vagrant::provider::lxc") + @logger.outputters = Log4r::Outputter.stdout + if config['output'] + @logger.outputters << Log4r::FileOutputter.new('output', 'filename' => config['output']) + end + # @logger.level = Log4r::INFO + end + + # @see Vagrant::Plugin::V1::Provider#action + def action(name, *args) + # Attempt to get the action method from this class if it + # exists, otherwise return nil to show that we don't support the + # given action. + action_method = "action_#{name}" + return send(action_method, *args) if respond_to?(action_method) + nil + end + + protected + + def run(cmd) + @logger.debug "Running: #{cmd}" + system cmd + end + + def action_up + was_created = container_created? + if was_created + @logger.info("Container already created, moving on...") + else + @logger.info("Creating container...") + # TRY: run 'sudo lxc-create -t ubuntu -n vagrant-container -b vagrant' + # DISCUSS: Copy key directly to /var/lib/lxc/$host/root/.ssh/authorized_keys to be generic? + unless run 'sudo lxc-create -t ubuntu-cloud -n vagrant-container -- -S /home/vagrant/.ssh/id_rsa.pub' + puts 'Error creating box' + exit 1 + end + unless container_created? + puts 'Error creating container' + exit 1 + end + end + + if container_started? + @logger.info('Container already started') + else + share_folders + + @logger.info('Starting container...') + unless run "sudo lxc-start -n vagrant-container -d #{configs}"# -o /tmp/lxc-start.log -l DEBUG" + puts 'Error starting container!' + exit 1 + end + run 'sudo lxc-wait --name vagrant-container --state RUNNING' + unless container_started? + puts 'Error starting container!' + exit 1 + end + @logger.info('Container started') + + forward_ports + + unless was_created + @logger.debug "Waiting #{WAIT} seconds before setting up vagrant user" + sleep WAIT + setup_vagrant_user + end + end + end + + def action_halt + if container_started? + @logger.info('Stopping container...') + unless run 'sudo lxc-shutdown -n vagrant-container' + puts 'Error halting container!' + exit 1 + end + run 'sudo lxc-wait --name vagrant-container --state STOPPED' + if container_started? + puts 'Error halting container!' + exit 1 + end + @logger.info('Container halted') + else + @logger.info('Container already halted') + end + end + + def action_destroy + if container_created? + if container_started? + action_halt + @logger.debug "Waiting #{WAIT} seconds to proceed with destroy..." + sleep WAIT + end + @logger.info("Destroying container...") + unless run 'sudo lxc-destroy -n vagrant-container' + puts 'Error destroying container' + exit 1 + end + if container_created? + puts 'Error destroying container' + exit 1 + end + @logger.debug "Waiting #{WAIT} seconds for things to settle down..." + sleep WAIT + @logger.info("Container destroyed") + else + @logger.info("Container not created") + end + end + + def action_reload + action_halt if container_started? + action_up + end + + # TODO: Switch over to Net:SSH + def action_ssh(opts = {'user' => 'vagrant'}) + # FIXME: We should not depend on an IP to be configured + raise 'SSH support is currently available to a predefined IP only' unless @config['ip'] + + cmd = "ssh #{opts['user']}@#{@config['ip']} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=quiet" + cmd << " -- #{Shellwords.escape opts['command']}" if opts['command'] + + unless run(cmd) + puts 'Error running ssh command!' + exit 1 + end + end + + def setup_vagrant_user + unless @config['ip'] + # FIXME: Need to find a way to grab the container IP + @logger.warn('Unfortunately automatic vagrant user setup does not work unless an IP is specified') + return + end + + @logger.info 'Setting up vagrant user' + + # TODO: We could try to use lxc-attach instead of SSH + + # Based on: + # https://github.com/jedi4ever/veewee/blob/master/templates/ubuntu-12.10-server-amd64-packages/vagrant.sh + cmds = [ + #'groupadd -r admin', + 'useradd -d /home/vagrant -m vagrant -r -s /bin/bash', + 'usermod -a -G admin vagrant', + 'cp /etc/sudoers /etc/sudoers.orig', + 'sed -i -e \'/Defaults\s\+env_reset/a Defaults\texempt_group=admin\' /etc/sudoers', + 'sed -i -e \'s/%admin\s\+ALL=(ALL)\s\+ALL/%admin ALL=NOPASSWD:ALL/g\' /etc/sudoers', + 'service sudo restart', + '-u vagrant -- mkdir -p /home/vagrant/.ssh', + '-u vagrant -- curl -o /home/vagrant/.ssh/authorized_keys https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub' + ] + + # FIXME: Needs to abort the process if any of this commands fail + ssh_conn('ubuntu') do |ssh| + cmds.each do |cmd| + @logger.debug "SSH: sudo #{cmd}" + ssh.exec!("sudo #{cmd}") + end + end + end + + def ssh_conn(user = 'vagrant') + Net::SSH.start(@config['ip'], user, :user_known_hosts_file => '/dev/null') do |ssh| + yield ssh + end + end + + def container_created? + `lxc-ls` =~ /^vagrant\-container/ + end + + def container_started? + `sudo -- lxc-info -n vagrant-container` =~ /RUNNING/ + end + + def share_folders + @logger.info('Setting up shared folders...') + + mount_folder(File.expand_path('.'), '/vagrant') + + Array(@config['shared_folders']).each do |folder| + mount_folder(folder['source'], folder['destination']) + end + end + + def mount_folder(source, destination) + @logger.info("Sharing #{source} as #{destination}") + run < #{host_port}") + forwards << "0.0.0.0 #{host_port} #{@config['ip']} #{guest_port}" + end + + # FIXME: We should be nice to others and not overwrite the config all the time ;) + File.open('/etc/rinetd.conf', 'w') do |f| + f.puts forwards + f.puts 'logfile /var/log/rinetd.log' + end + @logger.info('Restarting rinetd') + `sudo service rinetd restart` + end +end + +raise 'You need to provide an action' unless ARGV[0] + +action = ARGV.shift.to_sym +if action == :ssh + options = {'user' => 'vagrant'} + OptionParser.new do |opts| + opts.on("-c", '--command [COMMAND]') { |v| options['command'] = v } + opts.on('-u', '--user [USER]') { |v| options['user'] = v } + end.parse! + arguments = [options] +else + init_options = {} + OptionParser.new do |opts| + opts.on("-o", '--output [FILE]') { |v| init_options['output'] = v } + end.parse! +end + +config = YAML.load File.open('./config.yml') if File.exists? './config.yml' +config ||= {} +config['output'] = init_options.delete('output') if init_options && init_options.key?('output') + +@provider = Provider.new(config || {}) + +@provider.action(action, *(arguments || [])) diff --git a/setup-vagrant-dev-box b/setup-vagrant-dev-box new file mode 100755 index 0000000..f6ee4b4 --- /dev/null +++ b/setup-vagrant-dev-box @@ -0,0 +1,101 @@ +#!/usr/bin/env ruby + +raise 'You should not run this script from the dev box' if ENV['USER'] == 'vagrant' + +require 'bundler' +require 'json' + +IMAGE_ROOT = 'https://cloud-images.ubuntu.com/releases/quantal/release-20130206' +IMAGE_NAME = 'ubuntu-12.10-server-cloudimg-amd64-root.tar.gz' +VAGRANT_REPO = 'https://raw.github.com/mitchellh/vagrant/master' + +def download(source, destination) + destination = "#{File.dirname __FILE__}/cache/#{destination}" + return if File.exists?(destination) + + sh "wget #{source} -O #{destination}" +end + +def sh(cmd) + Bundler.with_clean_env do + puts cmd + raise 'Errored!' unless system cmd + end +end + +def restore_snapshot! + sh 'vagrant halt -f' + conf = JSON.parse File.read('.vagrant') + id = conf['active']['default'] + sh "VBoxManage snapshot '#{id}' restore ready-to-rock" + sh 'vagrant up' + exit 0 +end + +# Initialize git submodules +sh 'git submodule update --init' + +Bundler.with_clean_env do + # Ensure box has not been created yet + unless `vagrant status` =~ /not created/ + print 'Vagrant box already created, do you want to [r]ecreate it, restore [s]napshot or [A]bort? ' + answer = gets.chomp + exit 0 if answer.empty? || answer =~ /^a/i + + case + when answer =~ /^s/i + restore_snapshot! + when answer =~ /^r/i + sh 'vagrant destroy -f' + else + puts 'Invalid option!' + exit 1 + end + end +end + +# Cache development dependencies +`mkdir -p cache` + +# Cache container image between vagrant box destructions +download "#{IMAGE_ROOT}/#{IMAGE_NAME}", IMAGE_NAME + +# Start vagrant +sh 'vagrant up' + +# Because I'm lazy ;) +sh 'vagrant ssh -c "echo \'cd /vagrant\' >> ~/.bashrc"' + +# "be" archive is too slow for me +sh 'vagrant ssh -c "sudo sed -i -e \'s/be.archive/br.archive/g\' /etc/apt/sources.list"' + +# Ensure we have the latest packages around +sh 'vagrant ssh -c "sudo apt-get update && sudo apt-get upgrade -y"' + +# Ensure the machine can boot properly after upgrades +sh 'vagrant reload' + +# Install lxc, libffi, rinetd and bundler +sh 'vagrant ssh -c "sudo apt-get install lxc rinetd libffi-dev libffi-ruby ruby1.9.1-dev htop -y && sudo gem install bundler --no-ri --no-rdoc"' + +# Backup rinetd config +sh "vagrant ssh -c 'cp /etc/rinetd.conf /vagrant/cache/rinetd.conf'" + +# Make rinetd writable by vagrant user +sh "vagrant ssh -c 'sudo chown vagrant:vagrant /etc/rinetd.conf'" + +# Bundle! +sh "vagrant ssh -c 'cd /vagrant && bundle'" + +# Setup vagrant default ssh key +sh 'vagrant ssh -c "cp /vagrant/vagrant-1.1/keys/vagrant ~/.ssh/id_rsa && cp /vagrant/vagrant-1.1/keys/vagrant.pub ~/.ssh/id_rsa.pub && chmod 600 ~/.ssh/id_rsa"' + +# Setup lxc cache +sh "vagrant ssh -c 'sudo mkdir -p /var/cache/lxc/cloud-quantal && sudo cp /vagrant/cache/#{IMAGE_NAME} /var/cache/lxc/cloud-quantal/#{IMAGE_NAME}'" + +# Click +sh 'vagrant halt' +conf = JSON.parse File.read('.vagrant') +id = conf['active']['default'] +sh "VBoxManage snapshot '#{id}' take ready-to-rock" +sh 'vagrant up' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..3380bd3 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,59 @@ +require 'rubygems' + +require 'bundler/setup' + +Bundler.require + +require 'yaml' +require 'shellwords' + +`mkdir -p tmp` + +module TestHelpers + def provider_up + `cd tmp && ../lib/provider up -o /vagrant/tmp/logger.log` + end + + def destroy_container! + `cd tmp && ../lib/provider destroy -o /vagrant/tmp/logger.log` + `rm -f tmp/config.yml` + end + + def restore_rinetd_conf! + `sudo cp /vagrant/cache/rinetd.conf /etc/rinetd.conf` + `sudo service rinetd restart` + end + + def configure_box_with(opts) + opts = opts.dup + opts.keys.each do |key| + opts[key.to_s] = opts.delete(key) + end + File.open('./tmp/config.yml', 'w') { |f| f.puts YAML::dump(opts) } + end + + def provider_ssh(options) + options = options.map { |opt, val| "-#{opt} #{Shellwords.escape val}" } + options = options.join(' ') + `cd tmp && ../lib/provider ssh #{options}` + end +end + +RSpec.configure do |config| + config.treat_symbols_as_metadata_keys_with_true_values = true + config.run_all_when_everything_filtered = true + config.filter_run :focus + + config.include TestHelpers + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = 'random' + + config.after :all do + destroy_container! + restore_rinetd_conf! + end +end diff --git a/spec/vagrant_ssh_spec.rb b/spec/vagrant_ssh_spec.rb new file mode 100644 index 0000000..62f952f --- /dev/null +++ b/spec/vagrant_ssh_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe 'vagrant ssh' do + let(:ip) { '10.0.3.100' } + + before :all do + destroy_container! + configure_box_with :forwards => [[2222, 22]], :ip => ip + provider_up + end + + after :all do + restore_rinetd_conf! + destroy_container! + end + + it 'accepts a user argument' do + provider_ssh('c' => 'echo $USER', 'u' => 'ubuntu').should include 'ubuntu' + end +end diff --git a/spec/vagrant_up_spec.rb b/spec/vagrant_up_spec.rb new file mode 100644 index 0000000..191ae2b --- /dev/null +++ b/spec/vagrant_up_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' + +describe 'vagrant up' do + context 'given the machine has not been created yet' do + let(:output) { @output } + let(:containers) { @containers } + let(:users) { File.read '/var/lib/lxc/vagrant-container/rootfs/etc/passwd' } + let(:sudoers) { `sudo cat /var/lib/lxc/vagrant-container/rootfs/etc/sudoers` } + let(:rinetd_conf) { File.read('/etc/rinetd.conf') } + + before :all do + destroy_container! + configure_box_with :ip => '10.0.3.121' + @output = provider_up + @containers = `sudo lxc-ls`.split + end + + it 'outputs some debugging info' do + output.should =~ /INFO lxc: Creating container.../ + output.should =~ /INFO lxc: Container started/ + end + + it 'creates an lxc container' do + containers.should include 'vagrant-container' + end + + it 'sets up the vagrant user with passwordless sudo' do + users.should =~ /vagrant/ + sudoers.should =~ /Defaults\s+exempt_group=admin/ + sudoers.should =~ /%admin ALL=NOPASSWD:ALL/ + end + + it 'automagically shares the root folder' do + output.should =~ /Sharing \/vagrant\/tmp as \/vagrant/ + end + + it 'automagically redirects 2222 port to 22 on guest machine' + end + + context 'given the machine was created and is down' do + let(:output) { @output } + let(:info) { @info } + + before :all do + destroy_container! + provider_up + `sudo lxc-stop -n vagrant-container` + @output = provider_up + @info = `sudo lxc-info -n vagrant-container` + end + + it 'outputs some debugging info' do + output.should =~ /INFO lxc: Container already created, moving on/ + output.should =~ /INFO lxc: Container started/ + end + + it 'starts the container' do + info.should =~ /RUNNING/ + end + end + + context 'given the machine is up already' do + let(:output) { @output } + let(:containers) { @containers } + + before :all do + destroy_container! + provider_up + @output = provider_up + end + + it 'outputs some debugging info' do + output.should =~ /INFO lxc: Container already created, moving on/ + output.should =~ /INFO lxc: Container already started/ + end + end + + context 'given an ip was specified' do + let(:ip) { '10.0.3.100' } + let(:output) { @output } + + before :all do + destroy_container! + configure_box_with :ip => ip + @output = provider_up + end + + it 'sets up container ip' do + `ping -c1 #{ip} > /dev/null && echo -n 'yes'`.should == 'yes' + end + end + + context 'given a port was configured to be forwarded' do + let(:ip) { '10.0.3.101' } + let(:output) { @output } + let(:rinetd_conf) { File.read('/etc/rinetd.conf') } + + before :all do + destroy_container! + configure_box_with :forwards => [[3333, 33]], :ip => ip + @output = provider_up + end + + after :all do + restore_rinetd_conf! + end + + it 'ouputs some debugging info' do + output.should =~ /Forwarding ports\.\.\./ + output.should =~ /33 => 3333/ + output.should =~ /Restarting rinetd/ + end + + it 'sets configs for rinetd' do + rinetd_conf.should =~ /0\.0\.0\.0\s+3333\s+#{Regexp.escape ip}\s+33/ + end + end + + context 'given a folder was configured to be shared' do + let(:ip) { '10.0.3.100' } + let(:output) { @output } + + before :all do + destroy_container! + configure_box_with({ + :ip => ip, + :shared_folders => [ + {'source' => '/vagrant', 'destination' => '/tmp/vagrant-all'} + ] + }) + @output = provider_up + `rm -f /vagrant/tmp/file-from-spec` + end + + after :all do + `rm -f /vagrant/tmp/file-from-spec` + end + + it 'ouputs some debugging info' do + output.should =~ /Sharing \/vagrant as \/tmp\/vagrant\-all/ + end + + it 'mounts the folder on the right path' do + `echo 'IT WORKS' > /vagrant/tmp/file-from-spec` + provider_ssh('c' => 'cat /tmp/vagrant-all/tmp/file-from-spec').should include 'IT WORKS' + end + end +end diff --git a/tasks/spec.rake b/tasks/spec.rake new file mode 100644 index 0000000..b72e1cf --- /dev/null +++ b/tasks/spec.rake @@ -0,0 +1,4 @@ +if ENV['USER'] == 'vagrant' + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) +end diff --git a/vagrant-1.1 b/vagrant-1.1 new file mode 160000 index 0000000..803269f --- /dev/null +++ b/vagrant-1.1 @@ -0,0 +1 @@ +Subproject commit 803269f7291719715011c5c76d66e20101f7af50