Browse Source

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.
master
Georg Hopp 9 years ago
commit
e391d9ba09
  1. 10
      .gitignore
  2. 12
      Gemfile
  3. 36
      README.md
  4. 3
      Rakefile
  5. 14
      bin/console
  6. 8
      bin/setup
  7. 16
      example_box/README.md
  8. 3
      example_box/metadata.json
  9. 1
      example_box/vagrant.pub
  10. 17
      gentoo.json
  11. BIN
      gentoo_001_lxd.box
  12. 10
      lib/vagrant/lxd.rb
  13. 52
      lib/vagrant/lxd/action.rb
  14. 28
      lib/vagrant/lxd/action/create.rb
  15. 34
      lib/vagrant/lxd/action/ensure_image.rb
  16. 25
      lib/vagrant/lxd/action/ensure_ssh.rb
  17. 31
      lib/vagrant/lxd/action/ensure_started.rb
  18. 25
      lib/vagrant/lxd/action/network.rb
  19. 15
      lib/vagrant/lxd/command.rb
  20. 261
      lib/vagrant/lxd/driver.rb
  21. 39
      lib/vagrant/lxd/plugin.rb
  22. 70
      lib/vagrant/lxd/provider.rb
  23. 5
      lib/vagrant/lxd/version.rb
  24. 33
      vagrant-lxd.gemspec

10
.gitignore

@ -0,0 +1,10 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/Vagrantfile

12
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

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

3
Rakefile

@ -0,0 +1,3 @@
require "rubygems"
require "bundler/setup"
Bundler::GemHelper.install_tasks

14
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

8
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

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

3
example_box/metadata.json

@ -0,0 +1,3 @@
{
"provider": "lxd"
}

1
example_box/vagrant.pub

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key

17
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"
}
]
}
]
}

BIN
gentoo_001_lxd.box

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

52
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:

28
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:

34
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:

25
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:

31
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:

25
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:

15
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:

261
lib/vagrant/lxd/driver.rb

@ -0,0 +1,261 @@
##
# Probably useful lxc commands
# - get mac address:
# lxc config get <container> 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 <container>
# - Box/Image management is completely integrated with lxd. All image commands
# are:
# lxc image <something>
# 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 <container> default eth0
#
# Further things... right now gentoo specific
#
# - Create vagrant user:
# lxc exec <container> -- useradd vagrant
# - Set password:
# lxc exec <container> -- chpasswd <<<vagrant:vagrant
# - Enable sshd service:
# lxc exec <container> -- rc-update add sshd default
# - Start sshd service manually:
# lxc exec <container> -- /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:

39
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:

70
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:

5
lib/vagrant/lxd/version.rb

@ -0,0 +1,5 @@
module Vagrant
module Lxd
VERSION = "0.0.1"
end
end

33
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
Loading…
Cancel
Save