Add support for using a sudo wrapper script

Closes #90
This commit is contained in:
Fabio Rehm 2013-07-28 02:17:05 -03:00
parent 98f1df47a1
commit d62a053674
11 changed files with 182 additions and 90 deletions

View file

@ -10,6 +10,7 @@ BACKWARDS INCOMPATIBILITIES:
FEATURES:
- Add support for salt-minion and add latest dev release for ubuntu codenamed saucy [#116](https://github.com/fgrehm/vagrant-lxc/pull/116)
- Add support for using a sudo wrapper script [#90](https://github.com/fgrehm/vagrant-lxc/issues/90)
IMPROVEMENTS:

View file

@ -97,6 +97,41 @@ set from container's configuration file that is usually kept under
For other configuration options, please check [lxc.conf manpages](http://manpages.ubuntu.com/manpages/quantal/man5/lxc.conf.5.html).
### Avoiding `sudo` passwords
This plugin requires **a lot** of `sudo`ing since [user namespaces](https://wiki.ubuntu.com/LxcSecurity)
are not supported on mainstream kernels. In order to work around that we can use
a really dumb Ruby wrapper script like the one below and add a `NOPASSWD` entry
to our `/etc/sudoers` file:
```ruby
#!/usr/bin/env ruby
exec ARGV.join(' ')
```
For example, you can save the code above under your `/usr/bin/lxc-vagrant-wrapper`,
turn it into an executable script by running `chmod +x /usr/bin/lxc-vagrant-wrapper`
and add the line below to your `/etc/sudoers` file:
```
USERNAME ALL=NOPASSWD:/usr/bin/lxc-vagrant-wrapper
```
In order to tell vagrant-lxc to use that script when `sudo` is needed, you can
pass in the path to the script as a configuration for the provider:
```ruby
Vagrant.configure("2") do |config|
config.vm.provider :lxc do |lxc|
lxc.sudo_wrapper = '/usr/bin/lxc-vagrant-wrapper'
end
end
```
If you want to set the `sudo_wrapper` globally, just add the code above to your
`~/.vagrant.d/Vagrantfile`.
### Base boxes
Please check [the wiki](https://github.com/fgrehm/vagrant-lxc/wiki/Base-boxes)
@ -108,8 +143,6 @@ base boxes and information on [how to build your own](https://github.com/fgrehm/
* The plugin does not detect forwarded ports collision, right now you are
responsible for taking care of that.
* There is a hell lot of `sudo`s involved and this will probably be around until
[user namespaces](https://wiki.ubuntu.com/LxcSecurity) are supported or I'm able to handle [#90](https://github.com/fgrehm/vagrant-lxc/issues/90)
* [Does not tell you if dependencies are not met](https://github.com/fgrehm/vagrant-lxc/issues/11)
(will probably just throw up some random error)
* + bunch of other [core features](https://github.com/fgrehm/vagrant-lxc/issues?labels=core&milestone=&page=1&state=open)

View file

@ -14,7 +14,7 @@ module Vagrant
if env[:machine].state.id == :stopped
@logger.debug 'Removing temporary files'
tmp_path = env[:machine].provider.driver.rootfs_path.join('tmp')
system "sudo rm -rf #{tmp_path}/*"
env[:machine].provider.sudo_wrapper.run('rm', '-rf', "#{tmp_path}/*")
end
end
end

View file

@ -6,8 +6,15 @@ module Vagrant
# @return [Array]
attr_reader :customizations
# A String that points to a file that acts as a wrapper for sudo commands.
#
# This allows us to have a single entry when whitelisting NOPASSWD commands
# on /etc/sudoers
attr_accessor :sudo_wrapper
def initialize
@customizations = []
@sudo_wrapper = UNSET_VALUE
end
# Customize the container by calling `lxc-start` with the given
@ -25,7 +32,24 @@ module Vagrant
@customizations << [key, value]
end
# TODO: At some point in the future it would be nice to validate these options
def finalize!
@sudo_wrapper = nil if @sudo_wrapper == UNSET_VALUE
end
def validate(machine)
errors = []
if @sudo_wrapper
hostpath = Pathname.new(@sudo_wrapper).expand_path(machine.env.root_path)
if ! hostpath.file?
errors << I18n.t('vagrant_lxc.sudo_wrapper_not_found', path: hostpath.to_s)
elsif ! hostpath.executable?
errors << I18n.t('vagrant_lxc.sudo_wrapper_not_executable', path: hostpath.to_s)
end
end
{ "lxc provider" => errors }
end
end
end
end

View file

@ -14,9 +14,10 @@ module Vagrant
attr_reader :container_name,
:customizations
def initialize(container_name, cli = CLI.new(container_name))
def initialize(container_name, sudo_wrapper, cli = nil)
@container_name = container_name
@cli = cli
@sudo_wrapper = sudo_wrapper
@cli = cli || CLI.new(sudo_wrapper, container_name)
@logger = Log4r::Logger.new("vagrant::provider::lxc::driver")
@customizations = []
end
@ -48,7 +49,7 @@ module Vagrant
unless guestpath.directory?
begin
@logger.debug("Guest path doesn't exist, creating: #{guestpath}")
system "sudo mkdir -p #{guestpath.to_s}"
@sudo_wrapper.run('mkdir', '-p', guestpath.to_s)
rescue Errno::EACCES
raise Vagrant::Errors::SharedFolderCreateFailed, :path => guestpath.to_s
end
@ -89,10 +90,11 @@ module Vagrant
Dir.chdir base_path do
@logger.info "Compressing '#{rootfs_path}' rootfs to #{target_path}"
system "sudo rm -f rootfs.tar.gz && sudo tar --numeric-owner -czf #{target_path} #{basename}/*"
@sudo_wrapper.run('rm', '-f', 'rootfs.tar.gz')
@sudo_wrapper.run('tar', '--numeric-owner', '-czf', target_path, "#{basename}/*")
@logger.info "Changing rootfs tarbal owner"
system "sudo chown #{ENV['USER']}:#{ENV['USER']} #{target_path}"
@sudo_wrapper.run('chown', "#{ENV['USER']}:#{ENV['USER']}", target_path)
end
target_path
@ -121,11 +123,11 @@ module Vagrant
tmp_template_path = templates_path.join("lxc-#{template_name}").to_s
@logger.debug 'Copying LXC template into place'
system(%Q[sudo su root -c "cp #{path} #{tmp_template_path}"])
@sudo_wrapper.run('cp', path, tmp_template_path)
yield template_name
ensure
system(%Q[sudo su root -c "rm #{tmp_template_path}"])
@sudo_wrapper.run('rm', tmp_template_path)
end
TEMPLATES_PATH_LOOKUP = %w(

View file

@ -5,9 +5,9 @@ module Vagrant
module LXC
class Driver
class Builder
def self.build(id)
version = CLI.new.version.match(/^(\d+\.\d+)\./)[1].to_f
Driver.new(id).tap do |driver|
def self.build(id, shell)
version = CLI.new(shell).version.match(/^(\d+\.\d+)\./)[1].to_f
Driver.new(id, shell).tap do |driver|
mod = version >= 0.8 ?
Driver::FetchIpWithAttach :
Driver::FetchIpFromDsnmasq

View file

@ -17,12 +17,10 @@ module Vagrant
end
end
# Include this so we can use `Subprocess` more easily.
include Vagrant::Util::Retryable
def initialize(name = nil)
@name = name
@logger = Log4r::Logger.new("vagrant::provider::lxc::container::cli")
def initialize(sudo_wrapper, name = nil)
@sudo_wrapper = sudo_wrapper
@name = name
@logger = Log4r::Logger.new("vagrant::provider::lxc::container::cli")
end
def list
@ -66,7 +64,7 @@ module Vagrant
end
def start(overrides = [], extra_opts = [])
options = overrides.map { |key, value| ["-s", "lxc.#{key}=#{value}"] }.flatten
options = overrides.map { |key, value| ["-s", "lxc.#{key}='#{value}'"] }.flatten
options += extra_opts if extra_opts
run :start, '-d', '--name', @name, *options
end
@ -110,56 +108,7 @@ module Vagrant
private
def run(command, *args)
execute('sudo', "lxc-#{command}", *args)
end
# TODO: Review code below this line, it was pretty much a copy and
# paste from VirtualBox base driver and has no tests
def execute(*command, &block)
# Get the options hash if it exists
opts = {}
opts = command.pop if command.last.is_a?(Hash)
tries = 0
tries = 3 if opts[:retryable]
sleep = opts.fetch(:sleep, 1)
# Variable to store our execution result
r = nil
retryable(:on => LXC::Errors::ExecuteError, :tries => tries, :sleep => sleep) do
# Execute the command
r = raw(*command, &block)
# If the command was a failure, then raise an exception that is
# nicely handled by Vagrant.
if r.exit_code != 0
if @interrupted
@logger.info("Exit code != 0, but interrupted. Ignoring.")
else
raise LXC::Errors::ExecuteError, :command => command.inspect
end
end
end
# Return the output, making sure to replace any Windows-style
# newlines with Unix-style.
r.stdout.gsub("\r\n", "\n")
end
def raw(*command, &block)
int_callback = lambda do
@interrupted = true
@logger.info("Interrupted.")
end
# Append in the options for subprocess
command << { :notify => [:stdout, :stderr] }
Vagrant::Util::Busy.busy(int_callback) do
Vagrant::Util::Subprocess.execute(*command, &block)
end
@sudo_wrapper.run("lxc-#{command}", *args)
end
end
end

View file

@ -3,6 +3,7 @@ require "log4r"
require "vagrant-lxc/action"
require "vagrant-lxc/driver"
require "vagrant-lxc/driver/builder"
require "vagrant-lxc/sudo_wrapper"
module Vagrant
module LXC
@ -16,6 +17,14 @@ module Vagrant
machine_id_changed
end
def sudo_wrapper
@shell ||= begin
wrapper = @machine.provider_config.sudo_wrapper
wrapper = Pathname(wrapper).expand_path(@machine.env.root_path).to_s if wrapper
SudoWrapper.new(wrapper)
end
end
# If the machine ID changed, then we need to rebuild our underlying
# container.
def machine_id_changed
@ -23,7 +32,7 @@ module Vagrant
begin
@logger.debug("Instantiating the container for: #{id.inspect}")
@driver = Driver::Builder.build(id)
@driver = Driver::Builder.build(id, self.sudo_wrapper)
@driver.validate!
rescue Driver::ContainerNotFound
# The container doesn't exist, so we probably have a stale

View file

@ -0,0 +1,69 @@
module Vagrant
module LXC
class SudoWrapper
# Include this so we can use `Subprocess` more easily.
include Vagrant::Util::Retryable
def initialize(wrapper_path = nil)
@wrapper_path = wrapper_path
@logger = Log4r::Logger.new("vagrant::lxc::shell")
end
def run(*command)
command.unshift @wrapper_path if @wrapper_path
execute *(['sudo'] + command)
end
private
# TODO: Review code below this line, it was pretty much a copy and
# paste from VirtualBox base driver and has no tests
def execute(*command, &block)
# Get the options hash if it exists
opts = {}
opts = command.pop if command.last.is_a?(Hash)
tries = 0
tries = 3 if opts[:retryable]
sleep = opts.fetch(:sleep, 1)
# Variable to store our execution result
r = nil
retryable(:on => LXC::Errors::ExecuteError, :tries => tries, :sleep => sleep) do
# Execute the command
r = raw(*command, &block)
# If the command was a failure, then raise an exception that is
# nicely handled by Vagrant.
if r.exit_code != 0
if @interrupted
@logger.info("Exit code != 0, but interrupted. Ignoring.")
else
raise LXC::Errors::ExecuteError, :command => command.inspect
end
end
end
# Return the output, making sure to replace any Windows-style
# newlines with Unix-style.
r.stdout.gsub("\r\n", "\n")
end
def raw(*command, &block)
int_callback = lambda do
@interrupted = true
@logger.info("Interrupted.")
end
# Append in the options for subprocess
command << { :notify => [:stdout, :stderr] }
Vagrant::Util::Busy.busy(int_callback) do
Vagrant::Util::Subprocess.execute(*command, &block)
end
end
end
end
end

View file

@ -3,6 +3,10 @@ require 'unit_helper'
require 'vagrant-lxc/driver/cli'
describe Vagrant::LXC::Driver::CLI do
let(:sudo_wrapper) { instance_double('Vagrant::LXC::SudoWrapper', run: true) }
subject { described_class.new(sudo_wrapper) }
describe 'list' do
let(:lxc_ls_out) { "dup-container\na-container dup-container" }
let(:result) { @result }
@ -41,7 +45,7 @@ describe Vagrant::LXC::Driver::CLI do
let(:config_file) { 'config' }
let(:template_args) { { '--extra-param' => 'param', '--other' => 'value' } }
subject { described_class.new(name) }
subject { described_class.new(sudo_wrapper, name) }
before do
subject.stub(:run) { |*args| @run_args = args }
@ -64,7 +68,7 @@ describe Vagrant::LXC::Driver::CLI do
describe 'destroy' do
let(:name) { 'a-container-for-destruction' }
subject { described_class.new(name) }
subject { described_class.new(sudo_wrapper, name) }
before do
subject.stub(:run)
@ -78,7 +82,7 @@ describe Vagrant::LXC::Driver::CLI do
describe 'start' do
let(:name) { 'a-container' }
subject { described_class.new(name) }
subject { described_class.new(sudo_wrapper, name) }
before do
subject.stub(:run)
@ -96,15 +100,15 @@ describe Vagrant::LXC::Driver::CLI do
it 'uses provided array to override container configs' do
subject.start([['config', 'value'], ['other', 'value']])
subject.should have_received(:run).with(:start, '-d', '--name', name,
'-s', 'lxc.config=value',
'-s', 'lxc.other=value'
'-s', "lxc.config='value'",
'-s', "lxc.other='value'"
)
end
end
describe 'shutdown' do
let(:name) { 'a-running-container' }
subject { described_class.new(name) }
subject { described_class.new(sudo_wrapper, name) }
before do
subject.stub(:run)
@ -118,7 +122,7 @@ describe Vagrant::LXC::Driver::CLI do
describe 'state' do
let(:name) { 'a-container' }
subject { described_class.new(name) }
subject { described_class.new(sudo_wrapper, name) }
before do
subject.stub(:run).and_return("state: STOPPED\npid: 2")
@ -138,7 +142,7 @@ describe Vagrant::LXC::Driver::CLI do
let(:name) { 'a-running-container' }
let(:command) { ['ls', 'cat /tmp/file'] }
let(:command_output) { 'folders list' }
subject { described_class.new(name) }
subject { described_class.new(sudo_wrapper, name) }
before do
subject.stub(run: command_output)

View file

@ -6,9 +6,9 @@ require 'vagrant-lxc/driver/cli'
describe Vagrant::LXC::Driver do
describe 'container name validation' do
let(:unknown_container) { described_class.new('unknown', cli) }
let(:valid_container) { described_class.new('valid', cli) }
let(:new_container) { described_class.new(nil) }
let(:unknown_container) { described_class.new('unknown', nil, cli) }
let(:valid_container) { described_class.new('valid', nil, cli) }
let(:new_container) { described_class.new(nil, nil) }
let(:cli) { instance_double('Vagrant::LXC::Driver::CLI', list: ['valid']) }
it 'raises a ContainerNotFound error if an unknown container name gets provided' do
@ -39,7 +39,7 @@ describe Vagrant::LXC::Driver do
let(:rootfs_tarball) { '/path/to/cache/rootfs.tar.gz' }
let(:cli) { instance_double('Vagrant::LXC::Driver::CLI', :create => true, :name= => true) }
subject { described_class.new(nil, cli) }
subject { described_class.new(nil, nil, cli) }
before do
subject.stub(:import_template).and_yield(template_name)
@ -62,7 +62,7 @@ describe Vagrant::LXC::Driver do
describe 'destruction' do
let(:cli) { instance_double('Vagrant::LXC::Driver::CLI', destroy: true) }
subject { described_class.new('name', cli) }
subject { described_class.new('name', nil, cli) }
before { subject.destroy }
@ -76,7 +76,7 @@ describe Vagrant::LXC::Driver do
let(:internal_customization) { ['internal', 'customization'] }
let(:cli) { instance_double('Vagrant::LXC::Driver::CLI', start: true) }
subject { described_class.new('name', cli) }
subject { described_class.new('name', nil, cli) }
before do
cli.stub(:transition_to).and_yield(cli)
@ -96,7 +96,7 @@ describe Vagrant::LXC::Driver do
describe 'halt' do
let(:cli) { instance_double('Vagrant::LXC::Driver::CLI', shutdown: true) }
subject { described_class.new('name', cli) }
subject { described_class.new('name', nil, cli) }
before do
cli.stub(:transition_to).and_yield(cli)
@ -124,7 +124,7 @@ describe Vagrant::LXC::Driver do
let(:cli_state) { :something }
let(:cli) { instance_double('Vagrant::LXC::Driver::CLI', state: cli_state) }
subject { described_class.new('name', cli) }
subject { described_class.new('name', nil, cli) }
it 'delegates to cli' do
subject.state.should == cli_state
@ -161,8 +161,9 @@ describe Vagrant::LXC::Driver do
let(:folders) { [shared_folder] }
let(:rootfs_path) { Pathname('/path/to/rootfs') }
let(:expected_guest_path) { "#{rootfs_path}/vagrant" }
let(:sudo_wrapper) { instance_double('Vagrant::LXC::SudoWrapper', run: true) }
subject { described_class.new('name') }
subject { described_class.new('name', sudo_wrapper) }
before do
subject.stub(rootfs_path: rootfs_path, system: true)
@ -170,7 +171,7 @@ describe Vagrant::LXC::Driver do
end
it "creates guest folder under container's rootfs" do
subject.should have_received(:system).with("sudo mkdir -p #{expected_guest_path}")
sudo_wrapper.should have_received(:run).with("mkdir", "-p", expected_guest_path)
end
it 'adds a mount.entry to its local customizations' do