commit e391d9ba09781f131e39246de9fb425e36895dd7 Author: Georg Hopp Date: Fri May 5 22:34:34 2017 +0200 Initial commit The LXD provider load a box definition, downloads the corresponding image, create a container and prepares it for vagrant ssh. So vagrant up brings the container to live including managed bridge networking and vagrant ssh enters the container. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d569197 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +/Vagrantfile diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3b15b01 --- /dev/null +++ b/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in vagrant-lxd.gemspec +#gemspec + +group :development do + gem "vagrant", git: "https://github.com/mitchellh/vagrant.git" +end + +group :plugins do + gem "vagrant-lxd", path: "." +end diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4bc8ea --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Vagrant::Lxd + +Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/vagrant/lxd`. To experiment with that code, run `bin/console` for an interactive prompt. + +TODO: Delete this and the text above, and describe your gem + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'vagrant-lxd' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install vagrant-lxd + +## Usage + +TODO: Write usage instructions here + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/vagrant-lxd. + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b13de42 --- /dev/null +++ b/Rakefile @@ -0,0 +1,3 @@ +require "rubygems" +require "bundler/setup" +Bundler::GemHelper.install_tasks diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..9c605cd --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "vagrant/lxd" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/example_box/README.md b/example_box/README.md new file mode 100644 index 0000000..11de021 --- /dev/null +++ b/example_box/README.md @@ -0,0 +1,16 @@ +# Vagrant LXD Example Box + +Vagrant providers each require a custom provider-specific box format. +This folder shows the example contents of a box for the `lxd` provider. +To turn this into a box: + +``` +$ tar cvzf lxd.box ./metadata.json ./vagrant.pub +``` + +The `lxd` provider right now just uses the default lxd images provided +by the lxd images: remote. Upon start these will be provisioned with an +vagrant ssh user and and the unsafe common pubkey of vagrant and +sshd will be enabled. + +Well, at least thats the idea for now. diff --git a/example_box/metadata.json b/example_box/metadata.json new file mode 100644 index 0000000..10ccc02 --- /dev/null +++ b/example_box/metadata.json @@ -0,0 +1,3 @@ +{ + "provider": "lxd" +} diff --git a/example_box/vagrant.pub b/example_box/vagrant.pub new file mode 100644 index 0000000..18a9c00 --- /dev/null +++ b/example_box/vagrant.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key diff --git a/gentoo.json b/gentoo.json new file mode 100644 index 0000000..c6db7e8 --- /dev/null +++ b/gentoo.json @@ -0,0 +1,17 @@ +{ + "name": "lxd/gentoo", + "description": "The latest gentoo LXD image.", + "versions": [ + { + "version": "0.0.1", + "providers": [ + { + "name": "lxd", + "url": "file:///data/ghopp/projects/vagrant/vagrant-lxd/gentoo_001_lxd.box", + "checksum_type": "sha1", + "checksum": "4f3d7bfe034fe9fb82179992fdc6803b6f96abfb" + } + ] + } + ] +} diff --git a/gentoo_001_lxd.box b/gentoo_001_lxd.box new file mode 100644 index 0000000..f84fafc Binary files /dev/null and b/gentoo_001_lxd.box differ diff --git a/lib/vagrant/lxd.rb b/lib/vagrant/lxd.rb new file mode 100644 index 0000000..8da8103 --- /dev/null +++ b/lib/vagrant/lxd.rb @@ -0,0 +1,10 @@ +require 'bundler' + +begin + require 'vagrant' +rescue LoadError + Bundler.require(:default, :development) +end + +require 'vagrant/lxd/version' +require 'vagrant/lxd/plugin' diff --git a/lib/vagrant/lxd/action.rb b/lib/vagrant/lxd/action.rb new file mode 100644 index 0000000..f015759 --- /dev/null +++ b/lib/vagrant/lxd/action.rb @@ -0,0 +1,52 @@ +require 'json' +require 'log4r' + +require 'vagrant/action/builder' + +module Vagrant + module Lxd + module Action + action_root = Pathname.new(File.expand_path("../action", __FILE__)) + autoload :Create, action_root.join("create") + autoload :EnsureImage, action_root.join("ensure_image") + autoload :EnsureSsh, action_root.join("ensure_ssh") + autoload :EnsureStarted, action_root.join("ensure_started") + autoload :Network, action_root.join("network") + + include Vagrant::Action::Builtin + + # This action boots the VM, assuming the VM is in a state that requires + # a bootup (i.e. not saved). + def self.action_up + Vagrant::Action::Builder.new.tap do |b| + b.use ConfigValidate + b.use Call, IsState, :not_created do |env, b2| + # If the VM is NOT created yet, then do the setup steps + if env[:result] + b2.use HandleBox + b2.use EnsureImage + b2.use Network + b2.use Create + end + end + b.use action_start + b.use EnsureSsh + end + end + + def self.action_start + Vagrant::Action::Builder.new.tap do |b| + b.use EnsureStarted + end + end + + def self.action_ssh + Vagrant::Action::Builder.new.tap do |b| + b.use SSHExec + end + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/action/create.rb b/lib/vagrant/lxd/action/create.rb new file mode 100644 index 0000000..cd96b6e --- /dev/null +++ b/lib/vagrant/lxd/action/create.rb @@ -0,0 +1,28 @@ +module Vagrant + module Lxd + module Action + class Create + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::lxd::action::create") + end + + def call(env) + driver = env[:machine].provider.driver + + if driver.container? + env[:ui].info "--- Container fount ---", :prefix => false + else + env[:ui].info "--- Create #{driver.name} ---", :prefix => false + driver.create + env[:ui].info "--- #{driver.name} created ---", :prefix => false + end + + @app.call(env) + end + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/action/ensure_image.rb b/lib/vagrant/lxd/action/ensure_image.rb new file mode 100644 index 0000000..b5ac043 --- /dev/null +++ b/lib/vagrant/lxd/action/ensure_image.rb @@ -0,0 +1,34 @@ +module Vagrant + module Lxd + module Action + class EnsureImage + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::lxd::action::ensure_image") + end + + def call(env) + box = env[:machine].box + driver = env[:machine].provider.driver + + env[:ui].info "--- check image for #{env[:machine].name} ---", + :prefix => false + if driver.image? + env[:ui].info "--- Image found ---", :prefix => false + else + env[:ui].info "--- Image NOT found (downloading) ---", + :prefix => false + driver.get_image("images") + env[:ui].info "--- Image download done ---", :prefix => false + # TODO maybe we need to check again if the image really exists + # now. + end + + @app.call(env) + end + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/action/ensure_ssh.rb b/lib/vagrant/lxd/action/ensure_ssh.rb new file mode 100644 index 0000000..2f88826 --- /dev/null +++ b/lib/vagrant/lxd/action/ensure_ssh.rb @@ -0,0 +1,25 @@ +module Vagrant + module Lxd + module Action + class EnsureSsh + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::lxd::action::ensure_started") + end + + def call(env) + driver = env[:machine].provider.driver + + env[:ui].info "--- #{env[:machine].box.directory} ---", + :prefix => false + driver.vagrant_user + driver.enable_ssh + + @app.call(env) + end + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/action/ensure_started.rb b/lib/vagrant/lxd/action/ensure_started.rb new file mode 100644 index 0000000..d00dd69 --- /dev/null +++ b/lib/vagrant/lxd/action/ensure_started.rb @@ -0,0 +1,31 @@ +module Vagrant + module Lxd + module Action + class EnsureStarted + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::lxd::action::ensure_started") + end + + def call(env) + driver = env[:machine].provider.driver + + if driver.state != :running + env[:ui].info "--- start #{driver.name} ---", + :prefix => false + driver.start + env[:ui].info "--- #{driver.name} started ---", + :prefix => false + else + env[:ui].info "--- #{driver.name} alreay running ---", + :prefix => false + end + + @app.call(env) + end + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/action/network.rb b/lib/vagrant/lxd/action/network.rb new file mode 100644 index 0000000..e6f3306 --- /dev/null +++ b/lib/vagrant/lxd/action/network.rb @@ -0,0 +1,25 @@ +module Vagrant + module Lxd + module Action + class Network + def initialize(app, env) + @app = app + @logger = Log4r::Logger.new("vagrant::lxd::action::network") + end + + def call(env) + ## + # Right now I ignore all network config for machines and connect + # them all to a single bridge called vagrantbr0. (Well the name + # is transparently used and may be changed if necessary. + # + env[:bridge] = env[:machine].provider.driver.bridge + + @app.call(env) + end + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/command.rb b/lib/vagrant/lxd/command.rb new file mode 100644 index 0000000..9a703dd --- /dev/null +++ b/lib/vagrant/lxd/command.rb @@ -0,0 +1,15 @@ +module Vagrant + module Lxd + class Command < Vagrant.plugin('2', :command) + # def initialize(argv, env) + # super argv, env + # end + + def execute + @env.ui.info("my own plugin", :prefix => false) + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/driver.rb b/lib/vagrant/lxd/driver.rb new file mode 100644 index 0000000..5a0dabf --- /dev/null +++ b/lib/vagrant/lxd/driver.rb @@ -0,0 +1,261 @@ +## +# Probably useful lxc commands +# - get mac address: +# lxc config get volatile.eth0.hwaddr +# - get a json formated list of containers: +# lxc list --format=json -c ns4tS,volatile.eth0.hwaddr:MAC +# It seems that -c is ignored when json is user so: +# lxc list --format=json +# We care only about local containers... so ignore the remote. +# We might only want to list specific container started by vagrant, thus we +# should prefix each container name by the term 'vagrant_' and list only +# containers matching that pattern. +# lxc list vagrant- --format=json +# - The json above also seems to hold all the config information, anyway +# another way to show all config values for a given container in a more +# human readable form is: +# lxc config show +# - Box/Image management is completely integrated with lxd. All image commands +# are: +# lxc image +# The only thing we might need to keep information over is the image name or +# id to be used... here a remote might also be useful, to be able to use +# different image sources. But probably our box files will be quite simple. +# Anyway, i have not now completely figured out how box files work. +# +# This is pretty much all for now.... start, stop, init, etc. are left for +# later. +# One other thought... it might or might not be a good idea to connect all +# vagrant vms to the same bridge interface created by vagrant... anyway this +# should be configurable in some way. +# +# test this with e.g.: +# ~> bundel exec irb +# irb(main):001:0> load 'lxd.rb' +# => true +# irb(main):002:0> Vagrant::Lxd::Driver.new('vagrant-gentoo').vmdata +# +# General note: use pp to make the hash human readable. +# +# Network related commands: +# +# - Create a bridged network: +# lxc network create vagrantbr0 +# Another example: +# lxc network create vagrantbr0 ipv6.address=none ipv4.address=10.0.3.1/24 ipv4.nat=true +# - Attach network to container: +# lxc network attach vagrantbr0 default eth0 +# +# Further things... right now gentoo specific +# +# - Create vagrant user: +# lxc exec -- useradd vagrant +# - Set password: +# lxc exec -- chpasswd << -- rc-update add sshd default +# - Start sshd service manually: +# lxc exec -- /etc/init.d/sshd start +# +require 'json' +require 'log4r' +require 'yaml' + +require 'vagrant/util/retryable' + +module Vagrant + module Lxd + class Driver + include Vagrant::Util::Retryable + + attr_reader :name + + def initialize(machine) + @machine = machine + @name = "vagrant-#{machine.name}" + @logger = Log4r::Logger.new("vagrant::provider::lxd::driver") + + # This flag is used to keep track of interrupted state (SIGINT) + @interrupted = false + @image = machine.box.name.split("/")[1] if machine.box + bridge + end + + # Get all available images and their aliases + def images + data = JSON.parse(execute("image", "list", "--format=json")) + Hash[data.collect do |d| + d["aliases"].collect { |d2| [d2["name"], d] } + end.flatten(1)] + end + + def image? + images.key? @image + end + + # Get infos about all existing containers + def containers + data = JSON.parse(execute("list", "--format=json")) + Hash[data.collect { |d| [d["name"], d] }] + end + + def container? + containers.key? @name + end + + # This one will get infos about the managed container. + def container_data + containers[@name] + end + + def network + container_data["state"]["network"] + end + + def ipv4 + network["eth0"]["addresses"].select do |d| + d["family"] == "inet" + end[0]["address"] + end + + def state + return :not_created if not container? + return :stopped if not container_data["state"] + container_data["state"]["status"].downcase.to_sym + end + + def get_image(remote) + return if image? # image already exists + + args = [ + "image", + "copy", + "#{remote}:#{@image}", + "local:", + "--copy-aliases" + ] + + execute(*args) + end + + def create + # network could be also attached right here if it turns out to be + # a good idea. + execute("init", @image, @name, "-n", @bridge["name"]) + end + + def start + if state != :runnning + execute("start", @name) + end + end + + def bridge + while not @bridge do + begin + @bridge = YAML.load(execute("network", "show", "vagrantbr0")) + rescue + execute("network", "create", "vagrantbr0") + end + end + @bridge + end + + def vagrant_user + pwent = [] + while pwent.empty? do + begin + pwent = execute( + "exec", @name, "getent", "passwd", "vagrant" + ).split(":") + rescue + execute("exec", @name, "--", "useradd", "-m", "vagrant") + end + end + execute( + "file", + "push", + "--uid=#{pwent[2]}", + "--gid=#{pwent[3]}", + "--mode=0400", + "#{@machine.box.directory}/vagrant.pub", + "#{@name}/#{pwent[5]}/.ssh/authorized_keys" + ) + end + + def enable_ssh + begin + execute("exec", @name, "--", "rc-update", "add", "sshd", "default") + execute("exec", @name, "--", "/etc/init.d/sshd", "start") + rescue + end + end + + # Taken from Virtualbox provider and modified in some parts. + # Execute the given subcommand for Lxc and return the output. + 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] + + # Variable to store our execution result + r = nil + + # Most probably retrying is of no use here... if the command does not + # work it most likely will not work for the second time anyway... + # I leave this because I guess that vagrant tries commands even it the + # container is not up and running at the current time. + retryable(on: Vagrant::Errors::ProviderNotUsable, tries: tries, sleep: 1) 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 Vagrant::Errors::ProviderNotUsable, + provider: 'lxd', + machine: @machine.name, + message: "\"#{command.inspect}\" failed", + command: command.inspect, + stderr: r.stderr, + stdout: r.stdout + end + end + end + + # Return the output, making sure to replace any Windows-style + # newlines with Unix-style. + r.stdout.gsub("\r\n", "\n") + end + + # Executes a command and returns the raw result object. + def raw(*command, &block) + int_callback = lambda do + @interrupted = true + + # We have to execute this in a thread due to trap contexts + # and locks. + Thread.new { @logger.info("Interrupted.") }.join + end + + # Append in the options for subprocess + command << { notify: [:stdout, :stderr] } + + Vagrant::Util::Busy.busy(int_callback) do + Vagrant::Util::Subprocess.execute('lxc', *command, &block) + end + rescue Vagrant::Util::Subprocess::LaunchError => e + raise Vagrant::Errors::ProviderNotUsable, + message: e.to_s + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/plugin.rb b/lib/vagrant/lxd/plugin.rb new file mode 100644 index 0000000..484a0cb --- /dev/null +++ b/lib/vagrant/lxd/plugin.rb @@ -0,0 +1,39 @@ +## +# Test with something like: +# ~> bundle exec vagrant ls +# +module Vagrant + module Lxd + class Plugin < Vagrant.plugin('2') + name "Lxd" + + description <<-DESC + Vagrant LXD provider + DESC + + provider(:lxd, priority: 7) do + require File.expand_path("../provider", __FILE__) + Provider + end + + #config(:lxd, :provider) do + # require File.expand_path("../config", __FILE__) + # Config + #end + + #synced_folder(:virtualbox) do + # require File.expand_path("../synced_folder", __FILE__) + # SyncedFolder + #end + + command 'ls' do + require File.expand_path("../command", __FILE__) + Command + end + + autoload :Action, File.expand_path("../action", __FILE__) + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/provider.rb b/lib/vagrant/lxd/provider.rb new file mode 100644 index 0000000..6b602aa --- /dev/null +++ b/lib/vagrant/lxd/provider.rb @@ -0,0 +1,70 @@ +require "log4r" + +module Vagrant + module Lxd + autoload :Driver, File.expand_path("../driver", __FILE__) + autoload :Action, File.expand_path("../action", __FILE__) + + class Provider < Vagrant.plugin('2', :provider) + attr_reader :driver + + def initialize(machine) + @logger = Log4r::Logger.new("vagrant::provider::lxd") + @machine = machine + @driver = Driver.new(@machine) + end + + # Returns the SSH info for accessing the LXD container. + def ssh_info + # If the VM is not running that we can't possibly SSH into it + return nil if state.id != :running + + # Return what we know. The host is always "127.0.0.1" because + # VirtualBox VMs are always local. The port we try to discover + # by reading the forwarded ports. + return { + host: @driver.ipv4, + port: "22" + } + end + + # Return the state of VirtualBox virtual machine by actually + # querying VBoxManage. + # + # @return [Symbol] + def state + # Determine the ID of the state here. + state_id = @driver.state + + # Translate into short/long descriptions + short = state_id.to_s.gsub("_", " ") + long = I18n.t("vagrant.commands.status.#{state_id}") + + # If we're not created, then specify the special ID flag + if state_id == :not_created + state_id = Vagrant::MachineState::NOT_CREATED_ID + end + + # Return the state + Vagrant::MachineState.new(state_id, short, long) + end + + # @see Vagrant::Plugin::V1::Provider#action + def action(name) + # Attempt to get the action method from the Action class if it + # exists, otherwise return nil to show that we don't support the + # given action. + action_method = "action_#{name}" + return Action.send(action_method) if Action.respond_to?(action_method) + nil + end + + def to_s + id = @machine.id ? @machine.id : "new VM" + "Lxd (#{id})" + end + end + end +end + +# vim: set et ts=2 sw=2: diff --git a/lib/vagrant/lxd/version.rb b/lib/vagrant/lxd/version.rb new file mode 100644 index 0000000..f412cd7 --- /dev/null +++ b/lib/vagrant/lxd/version.rb @@ -0,0 +1,5 @@ +module Vagrant + module Lxd + VERSION = "0.0.1" + end +end diff --git a/vagrant-lxd.gemspec b/vagrant-lxd.gemspec new file mode 100644 index 0000000..6e6acba --- /dev/null +++ b/vagrant-lxd.gemspec @@ -0,0 +1,33 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'vagrant/lxd/version' + +Gem::Specification.new do |spec| + spec.name = "vagrant-lxd" + spec.version = Vagrant::Lxd::VERSION + spec.authors = ["Georg Hopp"] + spec.email = ["hopp@silpion.de"] + + spec.summary = %q{Vagrant LXD provider.} + spec.homepage = "https://somewhere.de/" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.13" + spec.add_development_dependency "rake", "~> 10.0" +end