diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb6bfd..70c45a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/README.md b/README.md index df881a1..397e0d5 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/lib/vagrant-lxc/action/remove_temporary_files.rb b/lib/vagrant-lxc/action/remove_temporary_files.rb index 62e487d..81a2f11 100644 --- a/lib/vagrant-lxc/action/remove_temporary_files.rb +++ b/lib/vagrant-lxc/action/remove_temporary_files.rb @@ -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 diff --git a/lib/vagrant-lxc/config.rb b/lib/vagrant-lxc/config.rb index 6095973..2065bc8 100644 --- a/lib/vagrant-lxc/config.rb +++ b/lib/vagrant-lxc/config.rb @@ -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 diff --git a/lib/vagrant-lxc/driver.rb b/lib/vagrant-lxc/driver.rb index 0a46194..791c535 100644 --- a/lib/vagrant-lxc/driver.rb +++ b/lib/vagrant-lxc/driver.rb @@ -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( diff --git a/lib/vagrant-lxc/driver/builder.rb b/lib/vagrant-lxc/driver/builder.rb index 35a8d1a..2f32faf 100644 --- a/lib/vagrant-lxc/driver/builder.rb +++ b/lib/vagrant-lxc/driver/builder.rb @@ -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 diff --git a/lib/vagrant-lxc/driver/cli.rb b/lib/vagrant-lxc/driver/cli.rb index b7ed4fe..ee9445c 100644 --- a/lib/vagrant-lxc/driver/cli.rb +++ b/lib/vagrant-lxc/driver/cli.rb @@ -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 diff --git a/lib/vagrant-lxc/provider.rb b/lib/vagrant-lxc/provider.rb index 74ef430..d8ea0ac 100644 --- a/lib/vagrant-lxc/provider.rb +++ b/lib/vagrant-lxc/provider.rb @@ -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 diff --git a/lib/vagrant-lxc/sudo_wrapper.rb b/lib/vagrant-lxc/sudo_wrapper.rb new file mode 100644 index 0000000..4bdb54a --- /dev/null +++ b/lib/vagrant-lxc/sudo_wrapper.rb @@ -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 diff --git a/spec/unit/driver/cli_spec.rb b/spec/unit/driver/cli_spec.rb index 5080fb2..0a0e65b 100644 --- a/spec/unit/driver/cli_spec.rb +++ b/spec/unit/driver/cli_spec.rb @@ -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) diff --git a/spec/unit/driver_spec.rb b/spec/unit/driver_spec.rb index f13dd34..c70f2dc 100644 --- a/spec/unit/driver_spec.rb +++ b/spec/unit/driver_spec.rb @@ -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