Initial commit

This commit is contained in:
pradels 2013-03-27 00:55:30 +01:00
commit 810f0b31b9
41 changed files with 1809 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
Vagrantfile
.vagrant

12
Gemfile Normal file
View File

@ -0,0 +1,12 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in vagrant-libvirt.gemspec
gemspec
group :development do
# We depend on Vagrant for development, but we don't add it as a
# gem dependency because we expect to be installed within the
# Vagrant environment itself using `vagrant plugin`.
gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git"
end

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2013 Lukas Stanek
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

178
README.md Normal file
View File

@ -0,0 +1,178 @@
# Vagrant Libvirt Provider
This is a [Vagrant](http://www.vagrantup.com) 1.1+ plugin that adds an
[Libvirt](http://libvirt.org) provider to Vagrant, allowing Vagrant to
control and provision machines via Libvirt toolkit.
This plugin is inspired by existing [vagrant-aws](https://github.com/mitchellh/vagrant-aws) provider.
**Note:** This plugin requires Vagrant 1.1+.
## Features
* Upload box image (qcow2 format) to Libvirt storage pool.
* Create volume as COW diff image for domains.
* Create and boot Libvirt domains.
* SSH into domains.
* Provision domains with any built-in Vagrant provisioner.
* Minimal synced folder support via `rsync`.
## Usage
Install using standard Vagrant 1.1+ plugin installation methods. After
installing, `vagrant up` and specify the `libvirt` provider. An example is
shown below.
```
$ vagrant plugin install vagrant-libvirt
...
$ vagrant up --provider=libvirt
...
```
Of course prior to doing this, you'll need to obtain an Libvirt-compatible
box file for Vagrant.
### Problems with plugin installation
In case of problems with building nokogiri gem, install missing development
libraries libxslt and libxml2.
In Ubuntu, Debian, ...
```
$ sudo apt-get install libxslt-dev libxml2-dev
```
In RedHat, Centos, Fedora, ...
```
# yum install libxslt-devel libxml2-devel
```
## Quick Start
After installing the plugin (instructions above), the quickest way to get
started is to add Libvirt box and specify all the details manually within
a `config.vm.provider` block. So first, add Libvirt box using any name you
want. This is just an example of Libvirt CentOS 6.4 box available:
```
$ vagrant box add centos64 http://kwok.cz/centos64.box
...
```
And then make a Vagrantfile that looks like the following, filling in
your information where necessary.
```
Vagrant.configure("2") do |config|
config.vm.define :test_vm do |test_vm|
test_vm.vm.box = "centos64"
end
config.vm.provider :libvirt do |libvirt|
libvirt.driver = "qemu"
libvirt.host = "example.com"
libvirt.connect_via_ssh = true
libvirt.username = "root"
#libvirt.password = "secret"
libvirt.storage_pool_name = "default"
end
end
```
And then run `vagrant up --provider=libvirt`.
This will first upload box image to remote Libvirt storage pool as new volume.
Then create and start a CentOS 6.4 domain on example.com Libvirt host. In this
example configuration, connection to Libvirt is tunneled via SSH.
## Box Format
Every provider in Vagrant must introduce a custom box format. This
provider introduces `Libvirt` boxes. You can view an example box in
the [example_box/directory](https://github.com/pradels/vagrant-libvirt/tree/master/example_box). That directory also contains instructions on how to build a box.
The box format is qcow2 image file `box.img`, the required `metadata.json` file
along with a `Vagrantfile` that does default settings for the
provider-specific configuration for this provider.
## Configuration
This provider exposes quite a few provider-specific configuration options:
* `driver` - A hypervisor name to access. For now only qemu is supported.
* `host` - The name of the server, where libvirtd is running.
* `connect_via_ssh` - If use ssh tunnel to connect to Libvirt.
* `username` - Username and password to access Libvirt.
* `password` - Password to access Libvirt.
* `storage_pool_name` - Libvirt storage pool name, where box image and
instance snapshots will be stored.
## Networks
Networking features in the form of `config.vm.network` are supported only
in bridged format, no hostonly network is supported in current version of
provider.
Example of network interface definition:
```
config.vm.define :test_vm do |test_vm|
test_vm.vm.network :bridged, :bridge => "default", :adapter => 1
end
```
Bridged network adapter connected to network `default` is defined.
## Getting IP address
There is a little problem to find out which IP address was assigned to remote
domain. Fog library uses SSH connection to remote libvirt host and by default
checks arpwatch entries there. Libvirt provider uses just arp table. There is no
need to install and setup arpwatch, but information about MAC->IP address
mapping is lost after short time. That is why there are no ssh or provision
actions available yet.
## Synced Folders
There is minimal support for synced folders. Upon `vagrant up`, the Libvirt
provider will use `rsync` (if available) to uni-directionally sync the folder
to the remote machine over SSH.
This is good enough for all built-in Vagrant provisioners (shell,
chef, and puppet) to work!
## Development
To work on the `vagrant-libvirt` plugin, clone this repository out, and use
[Bundler](http://gembundler.com) to get the dependencies:
```
$ bundle
```
Once you have the dependencies, verify the unit tests pass with `rake`:
```
$ bundle exec rake
```
If those pass, you're ready to start developing the plugin. You can test
the plugin without installing it into your Vagrant environment by just
creating a `Vagrantfile` in the top level of this directory (it is gitignored)
that uses it, and uses bundler to execute Vagrant:
```
$ bundle exec vagrant up --provider=libvirt
```
## Future work
* Read cpu and memory settings from config.
* Hostonly networks.
* Use Libvirt shared folder, not rsync if machine is local.
* Test if arpwatch is available for getting MAC->IP mapping.
* Provision, ssh, reload, halt, resume actions.
* Support other domain types than KVM.

7
Rakefile Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env rake
require 'rubygems'
require 'bundler/setup'
#require 'bundler/gem_tasks'
Bundler::GemHelper.install_tasks

23
example_box/README.md Normal file
View File

@ -0,0 +1,23 @@
# Vagrant Libvirt Example Box
Vagrant providers each require a custom provider-specific box format.
This folder shows the example contents of a box for the `libvirt` provider.
To turn this into a box create a vagrant image according documentation (don't
forget to install rsync command) and create box with following command:
```
$ tar cvzf custom_box.box ./metadata.json ./Vagrantfile ./box.img
```
This box works by using Vagrant's built-in Vagrantfile merging to setup
defaults for Libvirt. These defaults can easily be overwritten by higher-level
Vagrantfiles (such as project root Vagrantfiles).
## Box Metadata
Libvirt box should define at least three data fields in `metadata.json` file.
* provider - Provider name is libvirt.
* format - Currently supported format is qcow2.
* virtual_size - Virtual size of image in GBytes.

View File

@ -0,0 +1,5 @@
{
"provider" : "libvirt"
"format" : "qcow2"
"virtual_size" : "40"
}

30
lib/vagrant-libvirt.rb Normal file
View File

@ -0,0 +1,30 @@
require 'pathname'
require 'vagrant-libvirt/plugin'
module VagrantPlugins
module Libvirt
lib_path = Pathname.new(File.expand_path("../vagrant-libvirt", __FILE__))
autoload :Action, lib_path.join("action")
autoload :Errors, lib_path.join("errors")
autoload :Util, lib_path.join("util")
# Hold connection handler so there is no need to connect more times than
# one. This can be annoying when there are more machines to create, or when
# doing state action first and then some other.
#
# TODO Don't sure if this is the best solution
@@libvirt_connection = nil
def self.libvirt_connection
@@libvirt_connection
end
def self.libvirt_connection=(conn)
@@libvirt_connection = conn
end
def self.source_root
@source_root ||= Pathname.new(File.expand_path("../../", __FILE__))
end
end
end

View File

@ -0,0 +1,94 @@
require 'vagrant/action/builder'
module VagrantPlugins
module Libvirt
module Action
# Include the built-in modules so we can use them as top-level things.
include Vagrant::Action::Builtin
# This action is called to bring the box up from nothing.
def self.action_up
Vagrant::Action::Builder.new.tap do |b|
b.use ConfigValidate
b.use ConnectLibvirt
b.use Call, IsCreated do |env, b2|
if env[:result]
b2.use MessageAlreadyCreated
next
end
b2.use SetNameOfDomain
b2.use HandleStoragePool
b2.use HandleBoxImage
b2.use CreateDomainVolume
b2.use CreateDomain
b2.use CreateNetworkInterfaces
end
b.use TimedProvision
b.use StartDomain
b.use WaitTillUp
b.use SyncFolders
end
end
# This is the action that is primarily responsible for completely
# freeing the resources of the underlying virtual machine.
def self.action_destroy
Vagrant::Action::Builder.new.tap do |b|
b.use ConfigValidate
b.use Call, IsCreated do |env, b2|
if !env[:result]
b2.use MessageNotCreated
next
end
b2.use ConnectLibvirt
b2.use DestroyDomain
end
end
end
# This action is called to read the state of the machine. The resulting
# state is expected to be put into the `:machine_state_id` key.
def self.action_read_state
Vagrant::Action::Builder.new.tap do |b|
b.use ConfigValidate
b.use ConnectLibvirt
b.use ReadState
end
end
# This action is called to read the SSH info of the machine. The
# resulting state is expected to be put into the `:machine_ssh_info`
# key.
def self.action_read_ssh_info
Vagrant::Action::Builder.new.tap do |b|
b.use ConfigValidate
b.use ConnectLibvirt
b.use ReadSSHInfo
end
end
action_root = Pathname.new(File.expand_path("../action", __FILE__))
autoload :ConnectLibvirt, action_root.join("connect_libvirt")
autoload :IsCreated, action_root.join("is_created")
autoload :MessageAlreadyCreated, action_root.join("message_already_created")
autoload :MessageNotCreated, action_root.join("message_not_created")
autoload :HandleStoragePool, action_root.join("handle_storage_pool")
autoload :HandleBoxImage, action_root.join("handle_box_image")
autoload :SetNameOfDomain, action_root.join("set_name_of_domain")
autoload :CreateDomainVolume, action_root.join("create_domain_volume")
autoload :CreateDomain, action_root.join("create_domain")
autoload :CreateNetworkInterfaces, action_root.join("create_network_interfaces")
autoload :DestroyDomain, action_root.join("destroy_domain")
autoload :StartDomain, action_root.join("start_domain")
autoload :ReadState, action_root.join("read_state")
autoload :ReadSSHInfo, action_root.join("read_ssh_info")
autoload :TimedProvision, action_root.join("timed_provision")
autoload :WaitTillUp, action_root.join("wait_till_up")
autoload :SyncFolders, action_root.join("sync_folders")
end
end
end

View File

@ -0,0 +1,72 @@
require 'fog'
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
class ConnectLibvirt
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::connect_libvirt")
@app = app
end
def call(env)
# If already connected to libvirt, just use it and don't connect
# again.
if Libvirt.libvirt_connection
env[:libvirt_compute] = Libvirt.libvirt_connection
return @app.call(env)
end
# Get config options for libvirt provider.
config = env[:machine].provider_config
# Setup connection uri.
uri = config.driver
if config.connect_via_ssh
uri << '+ssh://'
if config.username
uri << config.username + '@'
end
if config.host
uri << config.host
else
uri << 'localhost'
end
else
uri << '://'
uri << config.host if config.host
end
uri << '/system?no_verify=1'
conn_attr = {}
conn_attr[:provider] = 'libvirt'
conn_attr[:libvirt_uri] = uri
conn_attr[:libvirt_username] = config.username if config.username
conn_attr[:libvirt_password] = config.password if config.password
# Setup command for retrieving IP address for newly created machine
# with some MAC address. Get it via arp table. This solution doesn't
# require arpwatch to be installed.
conn_attr[:libvirt_ip_command] = "arp -an | grep $mac | sed '"
conn_attr[:libvirt_ip_command] << 's/.*(\([0-9\.]*\)).*/\1/'
conn_attr[:libvirt_ip_command] << "'"
@logger.info("Connecting to Libvirt (#{uri}) ...")
begin
env[:libvirt_compute] = Fog::Compute.new(conn_attr)
rescue Fog::Errors::Error => e
raise Errors::FogLibvirtConnectionError,
:error_message => e.message
end
Libvirt.libvirt_connection = env[:libvirt_compute]
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,62 @@
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
class CreateDomain
include VagrantPlugins::Libvirt::Util::ErbTemplate
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::create_domain")
@app = app
end
def call(env)
# Gather some info about domain
# TODO from Vagrantfile
@name = env[:domain_name]
@cpus = 1
@memory_size = 512*1024
# TODO get type from driver config option
@domain_type = 'kvm'
@os_type = 'hvm'
# Get path to domain image.
domain_volume = Libvirt::Util::Collection.find_matching(
env[:libvirt_compute].volumes.all, "#{@name}.img")
raise Errors::DomainVolumeExists if domain_volume == nil
@domain_volume_path = domain_volume.path
# Output the settings we're going to use to the user
env[:ui].info(I18n.t("vagrant_libvirt.creating_domain"))
env[:ui].info(" -- Name: #{@name}")
env[:ui].info(" -- Domain type: #{@domain_type}")
env[:ui].info(" -- Cpus: #{@cpus}")
env[:ui].info(" -- Memory: #{@memory_size/1024}M")
env[:ui].info(" -- Base box: #{env[:machine].box.name}")
env[:ui].info(" -- Image: #{@domain_volume_path}")
# Create libvirt domain.
# Is there a way to tell fog to create new domain with already
# existing volume? Use domain creation from template..
begin
server = env[:libvirt_compute].servers.create(
:xml => to_xml('domain'))
rescue Fog::Errors::Error => e
raise Errors::FogCreateServerError,
:error_message => e.message
end
# Immediately save the ID since it is created at this point.
env[:machine].id = server.id
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,58 @@
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
# Create a snapshot of base box image. This new snapshot is just new
# cow image with backing storage pointing to base box image. Use this
# image as new domain volume.
class CreateDomainVolume
include VagrantPlugins::Libvirt::Util::ErbTemplate
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::create_domain_volume")
@app = app
end
def call(env)
env[:ui].info(I18n.t("vagrant_libvirt.creating_domain_volume"))
# Get config options.
config = env[:machine].provider_config
# This is name of newly created image for vm.
@name = "#{env[:domain_name]}.img"
# Verify the volume doesn't exist already.
domain_volume = Libvirt::Util::Collection.find_matching(
env[:libvirt_compute].volumes.all, @name)
raise Errors::DomainVolumeExists if domain_volume
# Get path to backing image - box volume.
box_volume = Libvirt::Util::Collection.find_matching(
env[:libvirt_compute].volumes.all, env[:box_volume_name])
@backing_file = box_volume.path
# Virtual size of image. Same as box image size.
@capacity = env[:machine].box.metadata['virtual_size'] #G
# Create new volume from xml template. Fog currently doesn't support
# volume snapshots directly.
begin
domain_volume = env[:libvirt_compute].volumes.create(
:xml => to_xml('volume_snapshot'),
:pool_name => config.storage_pool_name)
rescue Fog::Errors::Error => e
raise Errors::FogDomainVolumeCreateError,
:error_message => e.message
end
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,85 @@
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
# Create network interfaces for domain, before domain is running.
class CreateNetworkInterfaces
include VagrantPlugins::Libvirt::Util::ErbTemplate
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::create_network_interfaces")
@app = app
end
def call(env)
# Get domain first.
begin
domain = env[:libvirt_compute].client.lookup_domain_by_uuid(
env[:machine].id.to_s)
rescue => e
raise Errors::NoDomainError,
:error_message => e.message
end
# Setup list of interfaces before creating them
adapters = []
# Assign main interface for provisioning to first slot.
# Use network 'default' as network for ssh connecting and
# machine provisioning. This should be maybe configurable in
# Vagrantfile in future.
adapters[0] = 'default'
env[:machine].config.vm.networks.each do |type, options|
# Other types than bridged are not supported for now.
next if type != :bridged
network_name = 'default'
network_name = options[:bridge] if options[:bridge]
if options[:adapter]
if adapters[options[:adapter]]
raise Errors::InterfaceSlotNotAvailable
end
adapters[options[:adapter].to_i] = network_name
else
empty_slot = find_empty(adapters, start=1)
raise Errors::InterfaceSlotNotAvailable if empty_slot == nil
adapters[empty_slot] = network_name
end
end
# Create each interface as new domain device
adapters.each_with_index do |network_name, slot_number|
@iface_number = slot_number
@network_name = network_name
@logger.info("Creating network interface eth#{@iface_number}")
begin
domain.attach_device(to_xml('interface'))
rescue => e
raise Errors::AttachDeviceError,
:error_message => e.message
end
end
@app.call(env)
end
private
def find_empty(array, start=0, stop=8)
for i in start..stop
return i if !array[i]
end
return nil
end
end
end
end
end

View File

@ -0,0 +1,28 @@
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
class DestroyDomain
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::destroy_domain")
@app = app
end
def call(env)
# Destroy the server and remove the tracking ID
env[:ui].info(I18n.t("vagrant_libvirt.destroy_domain"))
domain = env[:libvirt_compute].servers.get(env[:machine].id.to_s)
domain.destroy(:destroy_volumes => true)
env[:machine].id = nil
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,121 @@
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
class HandleBoxImage
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::handle_box_image")
@app = app
end
def call(env)
# Verify box metadata for mandatory values.
#
# Virtual size has to be set for allocating space in storage pool.
box_virtual_size = env[:machine].box.metadata['virtual_size']
if box_virtual_size == nil
raise Errors::NoBoxVirtualSizeSet
end
# Support qcow2 format only for now, but other formats with backing
# store capability should be usable.
box_format = env[:machine].box.metadata['format']
if box_format == nil
raise Errors::NoBoxFormatSet
elsif box_format != 'qcow2'
raise Errors::WrongBoxFormatSet
end
# Get config options
config = env[:machine].provider_config
box_image_file = env[:machine].box.directory.join("box.img").to_s
env[:box_volume_name] = env[:machine].box.name.to_s.dup
env[:box_volume_name] << '_vagrant_box_image.img'
# Don't continue if image already exists in storage pool.
return @app.call(env) if Libvirt::Util::Collection.find_matching(
env[:libvirt_compute].volumes.all, env[:box_volume_name])
# Box is not available as a storage pool volume. Create and upload
# it as a copy of local box image.
env[:ui].info(I18n.t("vagrant_libvirt.uploading_volume"))
# Create new volume in storage pool
box_image_size = File.size(box_image_file) # B
message = "Creating volume #{env[:box_volume_name]}"
message << " in storage pool #{config.storage_pool_name}."
@logger.info(message)
begin
fog_volume = env[:libvirt_compute].volumes.create(
:name => env[:box_volume_name],
:allocation => "#{box_image_size/1024/1024}M",
:capacity => "#{box_virtual_size}G",
:format_type => box_format,
:pool_name => config.storage_pool_name)
rescue Fog::Errors::Error => e
raise Errors::FogCreateVolumeError,
:error_message => e.message
end
# Upload box image to storage pool
ret = upload_image(box_image_file, config.storage_pool_name,
env[:box_volume_name], env) do |progress|
env[:ui].clear_line
env[:ui].report_progress(progress, box_image_size, false)
end
# Clear the line one last time since the progress meter doesn't
# disappear immediately.
env[:ui].clear_line
# If upload failed or was interrupted, remove created volume from
# storage pool.
if env[:interrupted] or !ret
begin
fog_volume.destroy
rescue
nil
end
end
@app.call(env)
end
protected
# Fog libvirt currently doesn't support uploading images to storage
# pool volumes. Use ruby-libvirt client instead.
def upload_image(image_file, pool_name, volume_name, env)
image_size = File.size(image_file) # B
begin
pool = env[:libvirt_compute].client.lookup_storage_pool_by_name(
pool_name)
volume = pool.lookup_volume_by_name(volume_name)
stream = env[:libvirt_compute].client.stream
volume.upload(stream, offset=0, length=image_size)
buf_size = 1024*1024 # 1M
progress = 0
open(image_file, 'rb') do |io|
while (buff = io.read(buf_size)) do
sent = stream.send buff
progress += sent
yield progress
end
end
rescue => e
raise Errors::ImageUploadError,
:error_message => e.message
end
return true if progress == image_size
false
end
end
end
end
end

View File

@ -0,0 +1,49 @@
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
class HandleStoragePool
include VagrantPlugins::Libvirt::Util::ErbTemplate
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::handle_storage_pool")
@app = app
end
def call(env)
# Get config options.
config = env[:machine].provider_config
# Check for storage pool, where box image should be created
fog_pool = Libvirt::Util::Collection.find_matching(
env[:libvirt_compute].pools.all, config.storage_pool_name)
return @app.call(env) if fog_pool
@logger.info("No storage pool '#{config.storage_pool_name}' is available.")
# If user specified other pool than default, don't create default
# storage pool, just write error message.
raise Errors::NoStoragePool if config.storage_pool_name != 'default'
@logger.info("Creating storage pool 'default'")
# Fog libvirt currently doesn't support creating pools. Use
# ruby-libvirt client directly.
begin
libvirt_pool = env[:libvirt_compute].client.create_storage_pool_xml(
to_xml('default_storage_pool'))
rescue => e
raise Errors::CreatingStoragePoolError,
:error_message => e.message
end
raise Errors::NoStoragePool if !libvirt_pool
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,18 @@
module VagrantPlugins
module Libvirt
module Action
# This can be used with "Call" built-in to check if the machine
# is created and branch in the middleware.
class IsCreated
def initialize(app, env)
@app = app
end
def call(env)
env[:result] = env[:machine].state.id != :not_created
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,16 @@
module VagrantPlugins
module Libvirt
module Action
class MessageAlreadyCreated
def initialize(app, env)
@app = app
end
def call(env)
env[:ui].info(I18n.t("vagrant_libvirt.already_created"))
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,16 @@
module VagrantPlugins
module Libvirt
module Action
class MessageNotCreated
def initialize(app, env)
@app = app
end
def call(env)
env[:ui].info(I18n.t("vagrant_libvirt.not_created"))
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,51 @@
require "log4r"
module VagrantPlugins
module Libvirt
module Action
# This action reads the SSH info for the machine and puts it into the
# `:machine_ssh_info` key in the environment.
class ReadSSHInfo
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant_libvirt::action::read_ssh_info")
end
def call(env)
env[:machine_ssh_info] = read_ssh_info(
env[:libvirt_compute], env[:machine])
@app.call(env)
end
def read_ssh_info(libvirt, machine)
return nil if machine.id.nil?
# Find the machine
server = libvirt.servers.get(machine.id)
if server.nil?
# The machine can't be found
@logger.info("Machine couldn't be found, assuming it got destroyed.")
machine.id = nil
return nil
end
# Get ip address of machine
ip_address = server.public_ip_address
ip_address = server.private_ip_address if ip_address == nil
return nil if ip_address == nil
# Return the info
# TODO: Some info should be configurable in Vagrantfile
return {
:host => ip_address,
:port => 22,
:username => 'root',
}
end
end
end
end
end

View File

@ -0,0 +1,38 @@
require "log4r"
module VagrantPlugins
module Libvirt
module Action
# This action reads the state of the machine and puts it in the
# `:machine_state_id` key in the environment.
class ReadState
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant_libvirt::action::read_state")
end
def call(env)
env[:machine_state_id] = read_state(env[:libvirt_compute], env[:machine])
@app.call(env)
end
def read_state(libvirt, machine)
return :not_created if machine.id.nil?
# Find the machine
server = libvirt.servers.get(machine.id)
if server.nil? || [:"shutting-down", :terminated].include?(server.state.to_sym)
# The machine can't be found
@logger.info("Machine not found or terminated, assuming it got destroyed.")
machine.id = nil
return :not_created
end
# Return the state
return server.state.to_sym
end
end
end
end
end

View File

@ -0,0 +1,31 @@
module VagrantPlugins
module Libvirt
module Action
# Setup name for domain and domain volumes.
class SetNameOfDomain
def initialize(app, env)
@app = app
end
def call(env)
env[:domain_name] = env[:root_path].basename.to_s.dup
env[:domain_name].gsub!(/[^-a-z0-9_]/i, "")
env[:domain_name] << "_#{Time.now.to_i}"
# Check if the domain name is not already taken
domain = Libvirt::Util::Collection.find_matching(
env[:libvirt_compute].servers.all, env[:domain_name])
if domain != nil
raise Vagrant::Errors::DomainNameExists,
:domain_name => env[:domain_name]
end
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,27 @@
require 'log4r'
module VagrantPlugins
module Libvirt
module Action
# Just start the domain.
class StartDomain
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::start_domain")
@app = app
end
def call(env)
env[:ui].info(I18n.t("vagrant_libvirt.starting_domain"))
domain = env[:libvirt_compute].servers.get(env[:machine].id.to_s)
raise Errors::NoDomainError if domain == nil
domain.start
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,58 @@
require "log4r"
require "vagrant/util/subprocess"
module VagrantPlugins
module Libvirt
module Action
# This middleware uses `rsync` to sync the folders over to the
# libvirt domain.
class SyncFolders
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant_libvirt::action::sync_folders")
end
def call(env)
@app.call(env)
ssh_info = env[:machine].ssh_info
env[:machine].config.vm.synced_folders.each do |id, data|
hostpath = File.expand_path(data[:hostpath], env[:root_path])
guestpath = data[:guestpath]
# Make sure there is a trailing slash on the host path to
# avoid creating an additional directory with rsync
hostpath = "#{hostpath}/" if hostpath !~ /\/$/
env[:ui].info(I18n.t("vagrant_libvirt.rsync_folder",
:hostpath => hostpath,
:guestpath => guestpath))
# Create the guest path
env[:machine].communicate.sudo("mkdir -p '#{guestpath}'")
env[:machine].communicate.sudo(
"chown #{ssh_info[:username]} '#{guestpath}'")
# Rsync over to the guest path using the SSH info
command = [
"rsync", "--verbose", "--archive", "-z",
"--exclude", ".vagrant/",
"-e", "ssh -p #{ssh_info[:port]} -o StrictHostKeyChecking=no -i '#{ssh_info[:private_key_path]}'",
hostpath,
"#{ssh_info[:username]}@#{ssh_info[:host]}:#{guestpath}"]
r = Vagrant::Util::Subprocess.execute(*command)
if r.exit_code != 0
raise Errors::RsyncError,
:guestpath => guestpath,
:hostpath => hostpath,
:stderr => r.stderr
end
end
end
end
end
end
end

View File

@ -0,0 +1,21 @@
require "vagrant-libvirt/util/timer"
module VagrantPlugins
module Libvirt
module Action
# This is the same as the builtin provision except it times the
# provisioner runs.
class TimedProvision < Vagrant::Action::Builtin::Provision
def run_provisioner(env, p)
timer = Util::Timer.time do
super
end
env[:metrics] ||= {}
env[:metrics]["provisioner_times"] ||= []
env[:metrics]["provisioner_times"] << [p.class.to_s, timer]
end
end
end
end
end

View File

@ -0,0 +1,96 @@
require 'log4r'
require 'vagrant-libvirt/util/timer'
require 'vagrant/util/retryable'
module VagrantPlugins
module Libvirt
module Action
# Wait till domain is started, till it obtains an IP address and is
# accessible via ssh.
class WaitTillUp
include Vagrant::Util::Retryable
def initialize(app, env)
@logger = Log4r::Logger.new("vagrant_libvirt::action::wait_till_up")
@app = app
end
def call(env)
# Initialize metrics if they haven't been
env[:metrics] ||= {}
# Get domain object
domain = env[:libvirt_compute].servers.get(env[:machine].id.to_s)
raise NoDomainError if domain == nil
# Wait for domain to obtain an ip address. Ip address is searched
# from arp table, either localy or remotely via ssh, if libvirt
# connection was done via ssh.
env[:ip_address] = nil
env[:metrics]["instance_ip_time"] = Util::Timer.time do
env[:ui].info(I18n.t("vagrant_libvirt.waiting_for_ip"))
retryable(:on => Fog::Errors::TimeoutError, :tries => 300) do
# If we're interrupted don't worry about waiting
next if env[:interrupted]
# Wait for domain to obtain an ip address
domain.wait_for(2) {
addresses.each_pair do |type, ip|
env[:ip_address] = ip[0]
end
env[:ip_address] != nil
}
end
end
terminate(env) if env[:interrupted]
@logger.info("Got IP address #{env[:ip_address]}")
@logger.info("Time for getting IP: #{env[:metrics]["instance_ip_time"]}")
# Machine has ip address assigned, now wait till we are able to
# connect via ssh.
env[:metrics]["instance_ssh_time"] = Util::Timer.time do
env[:ui].info(I18n.t("vagrant_libvirt.waiting_for_ssh"))
retryable(:on => Fog::Errors::TimeoutError, :tries => 60) do
# If we're interrupted don't worry about waiting
next if env[:interrupted]
# Wait till we are able to connect via ssh.
while true
# If we're interrupted then just back out
break if env[:interrupted]
break if env[:machine].communicate.ready?
sleep 2
end
end
end
terminate(env) if env[:interrupted]
@logger.info("Time for SSH ready: #{env[:metrics]["instance_ssh_time"]}")
# Booted and ready for use.
#env[:ui].info(I18n.t("vagrant_libvirt.ready"))
@app.call(env)
end
def recover(env)
return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError)
if env[:machine].provider.state.id != :not_created
# Undo the import
terminate(env)
end
end
def terminate(env)
destroy_env = env.dup
destroy_env.delete(:interrupted)
destroy_env[:config_validate] = false
destroy_env[:force_confirm_destroy] = true
env[:action_runner].run(Action.action_destroy, destroy_env)
end
end
end
end
end

View File

@ -0,0 +1,48 @@
require 'vagrant'
module VagrantPlugins
module Libvirt
class Config < Vagrant.plugin('2', :config)
# A hypervisor name to access via Libvirt.
attr_accessor :driver
# The name of the server, where libvirtd is running.
attr_accessor :host
# If use ssh tunnel to connect to Libvirt.
attr_accessor :connect_via_ssh
# The username to access Libvirt.
attr_accessor :username
# Password for Libvirt connection.
attr_accessor :password
# Libvirt storage pool name, where box image and instance snapshots will
# be stored.
attr_accessor :storage_pool_name
def initialize
@driver = UNSET_VALUE
@host = UNSET_VALUE
@connect_via_ssh = UNSET_VALUE
@username = UNSET_VALUE
@password = UNSET_VALUE
@storage_pool_name = UNSET_VALUE
end
def finalize!
@driver = 'qemu' if @driver == UNSET_VALUE
@host = nil if @host == UNSET_VALUE
@connect_via_ssh = false if @connect_via_ssh == UNSET_VALUE
@username = nil if @username == UNSET_VALUE
@password = nil if @password == UNSET_VALUE
@storage_pool_name = 'default' if @storage_pool_name == UNSET_VALUE
end
def validate(machine)
end
end
end
end

View File

@ -0,0 +1,90 @@
require 'vagrant'
module VagrantPlugins
module Libvirt
module Errors
class VagrantLibvirtError < Vagrant::Errors::VagrantError
error_namespace("vagrant_libvirt.errors")
end
# Storage pools and volumes exceptions
class NoStoragePool < VagrantLibvirtError
error_key(:no_storage_pool)
end
class DomainVolumeExists < VagrantLibvirtError
error_key(:domain_volume_exists)
end
class NoDomainVolume < VagrantLibvirtError
error_key(:no_domain_volume)
end
class CreatingStoragePoolError < VagrantLibvirtError
error_key(:creating_storage_pool_error)
end
class ImageUploadError < VagrantLibvirtError
error_key(:image_upload_error_error)
end
# Box exceptions
class NoBoxVolume < VagrantLibvirtError
error_key(:no_box_volume)
end
class NoBoxVirtualSizeSet < VagrantLibvirtError
error_key(:no_box_virtual_size_error)
end
class NoBoxFormatSet < VagrantLibvirtError
error_key(:no_box_format_error)
end
class WrongBoxFormatSet < VagrantLibvirtError
error_key(:wrong_box_format_error)
end
# Fog libvirt exceptions
class FogLibvirtConnectionError < VagrantLibvirtError
error_key(:fog_libvirt_connection_error)
end
class FogCreateVolumeError < VagrantLibvirtError
error_key(:fog_create_volume_error)
end
class FogCreateDomainVolumeError < VagrantLibvirtError
error_key(:fog_create_domain_volume_error)
end
class FogCreateServerError < VagrantLibvirtError
error_key(:fog_create_server_error)
end
# Other exceptions
class InterfaceSlotNotAvailable < VagrantLibvirtError
error_key(:interface_slot_not_available)
end
class RsyncError < VagrantLibvirtError
error_key(:rsync_error)
end
class DomainNameExists < VagrantLibvirtError
error_key(:domain_name_exists_error)
end
class NoDomainError < VagrantLibvirtError
error_key(:no_domain_error)
end
class AttachDeviceError < VagrantLibvirtError
error_key(:attach_device_error)
end
end
end
end

View File

@ -0,0 +1,77 @@
begin
require 'vagrant'
rescue LoadError
raise "The Vagrant Libvirt plugin must be run within Vagrant."
end
# This is a sanity check to make sure no one is attempting to install
# this into an early Vagrant version.
if Vagrant::VERSION < '1.1.0'
raise "The Vagrant Libvirt plugin is only compatible with Vagrant 1.1+"
end
module VagrantPlugins
module Libvirt
class Plugin < Vagrant.plugin('2')
name "libvirt"
description <<-DESC
Vagrant plugin to manage VMs in libvirt.
DESC
config("libvirt", :provider) do
require_relative "config"
Config
end
provider "libvirt" do
# Setup logging and i18n
setup_logging
setup_i18n
require_relative "provider"
Provider
end
# This initializes the internationalization strings.
def self.setup_i18n
I18n.load_path << File.expand_path("locales/en.yml", Libvirt.source_root)
I18n.reload!
end
# This sets up our log level to be whatever VAGRANT_LOG is.
def self.setup_logging
require "log4r"
level = nil
begin
level = Log4r.const_get(ENV["VAGRANT_LOG"].upcase)
rescue NameError
# This means that the logging constant wasn't found,
# which is fine. We just keep `level` as `nil`. But
# we tell the user.
level = nil
end
# Some constants, such as "true" resolve to booleans, so the
# above error checking doesn't catch it. This will check to make
# sure that the log level is an integer, as Log4r requires.
level = nil if !level.is_a?(Integer)
# Set the logging level on all "vagrant" namespaced
# logs as long as we have a valid level.
if level
logger = Log4r::Logger.new("vagrant_libvirt")
logger.outputters = Log4r::Outputter.stderr
logger.level = level
logger = nil
end
end
end
end
end

View File

@ -0,0 +1,76 @@
require 'vagrant'
module VagrantPlugins
module Libvirt
# This is the base class for a provider for the V2 API. A provider
# is responsible for creating compute resources to match the
# needs of a Vagrant-configured system.
class Provider < Vagrant.plugin('2', :provider)
def initialize(machine)
@machine = machine
end
# This should return an action callable for the given name.
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
# This method is called if the underying machine ID changes. Providers
# can use this method to load in new data for the actual backing
# machine or to realize that the machine is now gone (the ID can
# become `nil`).
def machine_id_changed
end
# This should return a hash of information that explains how to
# SSH into the machine. If the machine is not at a point where
# SSH is even possible, then `nil` should be returned.
def ssh_info
# Run a custom action called "read_ssh_info" which does what it says
# and puts the resulting SSH info into the `:machine_ssh_info` key in
# the environment.
#
# Ssh info has following format..
#
#{
# :host => "1.2.3.4",
# :port => "22",
# :username => "mitchellh",
# :private_key_path => "/path/to/my/key"
#}
env = @machine.action("read_ssh_info")
env[:machine_ssh_info]
end
# This should return the state of the machine within this provider.
# The state must be an instance of {MachineState}.
def state
# Run a custom action we define called "read_state" which does
# what it says. It puts the state in the `:machine_state_id`
# key in the environment.
env = @machine.action("read_state")
state_id = env[:machine_state_id]
# Get the short and long description
short = I18n.t("vagrant_libvirt.states.short_#{state_id}")
long = I18n.t("vagrant_libvirt.states.long_#{state_id}")
# Return the MachineState object
Vagrant::MachineState.new(state_id, short, long)
end
def to_s
id = @machine.id.nil? ? "new" : @machine.id
"Libvirt (#{id})"
end
end
end
end

View File

@ -0,0 +1,13 @@
<pool type='dir'>
<name>default</name>
<source>
</source>
<target>
<path>/var/lib/libvirt/images</path>
<permissions>
<mode>0755</mode>
<owner>-1</owner>
<group>-1</group>
</permissions>
</target>
</pool>

View File

@ -0,0 +1,34 @@
<domain type='<%= @domain_type %>'>
<name><%= @name %></name>
<memory><%= @memory_size %></memory>
<vcpu><%= @cpus %></vcpu>
<os>
<type arch='x86_64'>hvm</type>
<boot dev='hd'/>
</os>
<features>
<acpi/>
<apic/>
<pae/>
</features>
<clock offset='utc'/>
<devices>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='<%= @domain_volume_path %>'/>
<%# we need to ensure a unique target dev -%>
<target dev='vda' bus='virtio'/>
</disk>
<serial type='pty'>
<target port='0'/>
</serial>
<console type='pty'>
<target port='0'/>
</console>
<input type='mouse' bus='ps2'/>
<graphics type='vnc' port='5900' autoport='yes' listen='127.0.0.1' keymap='en-us'/>
<video>
<model type='cirrus' vram='9216' heads='1'/>
</video>
</devices>
</domain>

View File

@ -0,0 +1,7 @@
<interface type='network'>
<source network='<%= @network_name %>'/>
<target dev='vnet<%= @iface_number %>'/>
<alias name='net<%= @iface_number %>'/>
<model type='virtio'/>
</interface>

View File

@ -0,0 +1,26 @@
<volume>
<name><%= @name %></name>
<capacity unit="G"><%= @capacity %></capacity>
<target>
<format type='qcow2'/>
<permissions>
<owner>0</owner>
<group>0</group>
<mode>0600</mode>
<label>virt_image_t</label>
</permissions>
</target>
<backingStore>
<path><%= @backing_file %></path>
<format type='qcow2'/>
<permissions>
<owner>0</owner>
<group>0</group>
<mode>0600</mode>
<label>virt_image_t</label>
</permissions>
</backingStore>
</volume>

View File

@ -0,0 +1,10 @@
module VagrantPlugins
module Libvirt
module Util
autoload :ErbTemplate, 'vagrant-libvirt/util/erb_template'
autoload :Collection, 'vagrant-libvirt/util/collection'
autoload :Timer, 'vagrant-libvirt/util/timer'
end
end
end

View File

@ -0,0 +1,22 @@
module VagrantPlugins
module Libvirt
module Util
module Collection
# This method finds a matching _thing_ in a collection of
# _things_. This works matching if the ID or NAME equals to
# `name`. Or, if `name` is a regexp, a partial match is chosen
# as well.
def self.find_matching(collection, name)
collection.each do |single|
return single if single.name == name
end
nil
end
end
end
end
end

View File

@ -0,0 +1,21 @@
require 'erb'
module VagrantPlugins
module Libvirt
module Util
module ErbTemplate
# Taken from fog source.
def to_xml template_name = nil
erb = template_name || self.class.to_s.split("::").last.downcase
path = File.join(File.dirname(__FILE__), "..", "templates",
"#{erb}.xml.erb")
template = File.read(path)
ERB.new(template, nil, '-').result(binding)
end
end
end
end
end

View File

@ -0,0 +1,17 @@
module VagrantPlugins
module Libvirt
module Util
class Timer
# A basic utility method that times the execution of the given
# block and returns it.
def self.time
start_time = Time.now.to_f
yield
end_time = Time.now.to_f
end_time - start_time
end
end
end
end
end

View File

@ -0,0 +1,5 @@
module VagrantPlugins
module Libvirt
VERSION = "0.0.1"
end
end

103
locales/en.yml Normal file
View File

@ -0,0 +1,103 @@
en:
vagrant_libvirt:
already_created: |-
The machine is already created.
not_created: |-
Machine is not created. Please run `vagrant up` first.
finding_volume: |-
Checking if volume is available.
creating_domain: |-
Creating machine with the following settings...
uploading_volume: |-
Uploading base box image as volume into libvirt storage...
creating_domain_volume: |-
Creating image (snapshot of base box volume).
removing_domain_volume: |-
Removing image (snapshot of base box volume).
starting_domain: |-
Starting machine.
terminating: |-
Removing machine...
poweroff_domain: |-
Poweroff machine.
destroy_domain: |-
Removing machine...
waiting_for_ready: |-
Waiting for machine to become "ready"...
waiting_for_ip: |-
Waiting for machine to get an IP address...
waiting_for_ssh: |-
Waiting for SSH to become available...
booted: |-
Machine is booted.
rsync_folder: |-
Rsyncing folder: %{hostpath} => %{guestpath}
ready: |-
Machine is booted and ready for use!
errors:
fog_error: |-
There was an error talking to Libvirt. The error message is shown
below:
%{message}
no_matching_volume: |-
No matching volume was found! Please check your volume setting
to make sure you have a valid volume chosen.
no_storage_pool: |-
No usable storage pool found! Please check if storage pool is
created and available.
no_box_volume: |-
Volume for box image is missing in storage pools. Try to run vagrant
again, or check if storage volume is accessible.
domain_volume_exists: |-
Volume for domain is already created. Please run 'vagrant destroy' first.
no_domain_volume: |-
Volume for domain is missing. Try to run 'vagrant up' again.
interface_slot_not_available: |-
Interface adapter number is already in use. Please specify other adapter
number.
rsync_error: |-
There was an error when attemping to rsync a share folder.
Please inspect the error message below for more info.
Host path: %{hostpath}
Guest path: %{guestpath}
Error: %{stderr}
no_box_virtual_size: |-
No image virtual size specified for box.
no_box_format: |-
No image format specified for box.
wrong_box_format: |-
Wrong image format specified for box.
fog_libvirt_connection_error: |-
Error while connecting to libvirt: %{error_message}
fog_create_volume_error: |-
Error while creating a storage pool volume: %{error_message}
fog_create_domain_volume_error: |-
Error while creating volume for domain: %{error_message}
fog_create_server_error: |-
Error while creating domain: %{error_message}
domain_name_exists: |-
Name of domain about to create is already taken. Please try to run
`vagrant up` command again.
creating_storage_pool_error: |-
There was error while creating libvirt storage pool: %{error_message}
image_upload_error: |-
Error while uploading image to storage pool: %{error_message}
no_domain_error: |-
No domain found. %{error_message}
attach_device_error: |-
Error while attaching new device to domain. %{error_message}
states:
short_not_created: |-
not created
long_not_created: |-
The Libvirt domain is not created. Run `vagrant up` to create it.
short_running: |-
running
long_running: |-
The Libvirt domain is running. To stop this machine, you can run
`vagrant halt`. To destroy the machine, you can run `vagrant destroy`.

23
vagrant-libvirt.gemspec Normal file
View File

@ -0,0 +1,23 @@
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/vagrant-libvirt/version', __FILE__)
Gem::Specification.new do |gem|
gem.authors = ["Lukas Stanek"]
gem.email = ["ls@elostech.cz"]
gem.description = %q{Vagrant provider for libvirt.}
gem.summary = %q{Vagrant provider for libvirt.}
gem.homepage = "http://www.vagrantup.com"
gem.files = `git ls-files`.split($\)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.name = "vagrant-libvirt"
gem.require_paths = ["lib"]
gem.version = VagrantPlugins::Libvirt::VERSION
gem.add_runtime_dependency "fog", "~> 1.10.0"
gem.add_runtime_dependency "ruby-libvirt", "~> 0.4.0"
gem.add_development_dependency "rake"
end