Not so initial commit

This commit is contained in:
Fabio Rehm 2013-02-25 01:58:04 -03:00
commit 55c9be772d
18 changed files with 777 additions and 0 deletions

21
.gitignore vendored Normal file
View file

@ -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

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "vagrant-1.1"]
path = vagrant-1.1
url = git://github.com/mitchellh/vagrant.git

2
.rspec Normal file
View file

@ -0,0 +1,2 @@
--color
--format documentation

1
.vimrc Normal file
View file

@ -0,0 +1 @@
set wildignore+=*/vagrant-1.1/*

14
Gemfile Normal file
View file

@ -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'

50
Gemfile.lock Normal file
View file

@ -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

10
Guardfile Normal file
View file

@ -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

37
README.md Normal file
View file

@ -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.

5
Rakefile Normal file
View file

@ -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

19
Vagrantfile vendored Normal file
View file

@ -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

5
config.yml.sample Normal file
View file

@ -0,0 +1,5 @@
---
ip: 10.0.3.100
forwards:
- - 2222
- 22

277
lib/provider Executable file
View file

@ -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 <<STR
sudo mount --bind #{source} #{source}
sudo mount --make-unbindable #{source}
sudo mount --make-shared #{source}
if ! [ -d /var/lib/lxc/vagrant-container/rootfs#{destination} ]; then
sudo mkdir -p /var/lib/lxc/vagrant-container/rootfs#{destination}
fi
if ! $(sudo grep -q '#{source} /var/lib/lxc/vagrant-container/rootfs#{destination}' /var/lib/lxc/vagrant-container/fstab); then
cat <<EOF | sudo tee -a /var/lib/lxc/vagrant-container/fstab
#{source} /var/lib/lxc/vagrant-container/rootfs#{destination} none bind 0 0
EOF
fi
STR
end
def configs
configs = []
configs << "-s lxc.network.ipv4='#{@config['ip']}'" if @config['ip']
configs << '-s lxc.cgroup.memory.limit_in_bytes=400M'
configs << '-s lxc.cgroup.memory.memsw.limit_in_bytes=500M'
configs.join(' ')
end
def forward_ports
return unless @config.key?('forwards')
@logger.info('Forwarding ports...')
forwards = ''
@config['forwards'].each do |forward|
host_port, guest_port = forward
@logger.info("-- #{guest_port} => #{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 || []))

101
setup-vagrant-dev-box Executable file
View file

@ -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'

59
spec/spec_helper.rb Normal file
View file

@ -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

20
spec/vagrant_ssh_spec.rb Normal file
View file

@ -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

148
spec/vagrant_up_spec.rb Normal file
View file

@ -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

4
tasks/spec.rake Normal file
View file

@ -0,0 +1,4 @@
if ENV['USER'] == 'vagrant'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
end

1
vagrant-1.1 Submodule

@ -0,0 +1 @@
Subproject commit 803269f7291719715011c5c76d66e20101f7af50