From 810f0b31b973281bdf70e8b9810f02d2486e9eb8 Mon Sep 17 00:00:00 2001 From: pradels Date: Wed, 27 Mar 2013 00:55:30 +0100 Subject: [PATCH] Initial commit --- .gitignore | 19 ++ Gemfile | 12 ++ LICENSE | 22 +++ README.md | 178 ++++++++++++++++++ Rakefile | 7 + example_box/README.md | 23 +++ example_box/metadata.json | 5 + lib/vagrant-libvirt.rb | 30 +++ lib/vagrant-libvirt/action.rb | 94 +++++++++ lib/vagrant-libvirt/action/connect_libvirt.rb | 72 +++++++ lib/vagrant-libvirt/action/create_domain.rb | 62 ++++++ .../action/create_domain_volume.rb | 58 ++++++ .../action/create_network_interfaces.rb | 85 +++++++++ lib/vagrant-libvirt/action/destroy_domain.rb | 28 +++ .../action/handle_box_image.rb | 121 ++++++++++++ .../action/handle_storage_pool.rb | 49 +++++ lib/vagrant-libvirt/action/is_created.rb | 18 ++ .../action/message_already_created.rb | 16 ++ .../action/message_not_created.rb | 16 ++ lib/vagrant-libvirt/action/read_ssh_info.rb | 51 +++++ lib/vagrant-libvirt/action/read_state.rb | 38 ++++ .../action/set_name_of_domain.rb | 31 +++ lib/vagrant-libvirt/action/start_domain.rb | 27 +++ lib/vagrant-libvirt/action/sync_folders.rb | 58 ++++++ lib/vagrant-libvirt/action/timed_provision.rb | 21 +++ lib/vagrant-libvirt/action/wait_till_up.rb | 96 ++++++++++ lib/vagrant-libvirt/config.rb | 48 +++++ lib/vagrant-libvirt/errors.rb | 90 +++++++++ lib/vagrant-libvirt/plugin.rb | 77 ++++++++ lib/vagrant-libvirt/provider.rb | 76 ++++++++ .../templates/default_storage_pool.xml.erb | 13 ++ lib/vagrant-libvirt/templates/domain.xml.erb | 34 ++++ .../templates/interface.xml.erb | 7 + .../templates/volume_snapshot.xml.erb | 26 +++ lib/vagrant-libvirt/util.rb | 10 + lib/vagrant-libvirt/util/collection.rb | 22 +++ lib/vagrant-libvirt/util/erb_template.rb | 21 +++ lib/vagrant-libvirt/util/timer.rb | 17 ++ lib/vagrant-libvirt/version.rb | 5 + locales/en.yml | 103 ++++++++++ vagrant-libvirt.gemspec | 23 +++ 41 files changed, 1809 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100644 example_box/README.md create mode 100644 example_box/metadata.json create mode 100644 lib/vagrant-libvirt.rb create mode 100644 lib/vagrant-libvirt/action.rb create mode 100644 lib/vagrant-libvirt/action/connect_libvirt.rb create mode 100644 lib/vagrant-libvirt/action/create_domain.rb create mode 100644 lib/vagrant-libvirt/action/create_domain_volume.rb create mode 100644 lib/vagrant-libvirt/action/create_network_interfaces.rb create mode 100644 lib/vagrant-libvirt/action/destroy_domain.rb create mode 100644 lib/vagrant-libvirt/action/handle_box_image.rb create mode 100644 lib/vagrant-libvirt/action/handle_storage_pool.rb create mode 100644 lib/vagrant-libvirt/action/is_created.rb create mode 100644 lib/vagrant-libvirt/action/message_already_created.rb create mode 100644 lib/vagrant-libvirt/action/message_not_created.rb create mode 100644 lib/vagrant-libvirt/action/read_ssh_info.rb create mode 100644 lib/vagrant-libvirt/action/read_state.rb create mode 100644 lib/vagrant-libvirt/action/set_name_of_domain.rb create mode 100644 lib/vagrant-libvirt/action/start_domain.rb create mode 100644 lib/vagrant-libvirt/action/sync_folders.rb create mode 100644 lib/vagrant-libvirt/action/timed_provision.rb create mode 100644 lib/vagrant-libvirt/action/wait_till_up.rb create mode 100644 lib/vagrant-libvirt/config.rb create mode 100644 lib/vagrant-libvirt/errors.rb create mode 100644 lib/vagrant-libvirt/plugin.rb create mode 100644 lib/vagrant-libvirt/provider.rb create mode 100644 lib/vagrant-libvirt/templates/default_storage_pool.xml.erb create mode 100644 lib/vagrant-libvirt/templates/domain.xml.erb create mode 100644 lib/vagrant-libvirt/templates/interface.xml.erb create mode 100644 lib/vagrant-libvirt/templates/volume_snapshot.xml.erb create mode 100644 lib/vagrant-libvirt/util.rb create mode 100644 lib/vagrant-libvirt/util/collection.rb create mode 100644 lib/vagrant-libvirt/util/erb_template.rb create mode 100644 lib/vagrant-libvirt/util/timer.rb create mode 100644 lib/vagrant-libvirt/version.rb create mode 100644 locales/en.yml create mode 100644 vagrant-libvirt.gemspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d036c0f --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..ebd9fc4 --- /dev/null +++ b/Gemfile @@ -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 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..93920f0 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e883d8 --- /dev/null +++ b/README.md @@ -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. + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b41e6e1 --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +#!/usr/bin/env rake + +require 'rubygems' +require 'bundler/setup' +#require 'bundler/gem_tasks' +Bundler::GemHelper.install_tasks + diff --git a/example_box/README.md b/example_box/README.md new file mode 100644 index 0000000..50b519e --- /dev/null +++ b/example_box/README.md @@ -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. + diff --git a/example_box/metadata.json b/example_box/metadata.json new file mode 100644 index 0000000..b755f46 --- /dev/null +++ b/example_box/metadata.json @@ -0,0 +1,5 @@ +{ + "provider" : "libvirt" + "format" : "qcow2" + "virtual_size" : "40" +} diff --git a/lib/vagrant-libvirt.rb b/lib/vagrant-libvirt.rb new file mode 100644 index 0000000..ad69975 --- /dev/null +++ b/lib/vagrant-libvirt.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action.rb b/lib/vagrant-libvirt/action.rb new file mode 100644 index 0000000..f1acc48 --- /dev/null +++ b/lib/vagrant-libvirt/action.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/connect_libvirt.rb b/lib/vagrant-libvirt/action/connect_libvirt.rb new file mode 100644 index 0000000..04a0f82 --- /dev/null +++ b/lib/vagrant-libvirt/action/connect_libvirt.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/create_domain.rb b/lib/vagrant-libvirt/action/create_domain.rb new file mode 100644 index 0000000..db58ec2 --- /dev/null +++ b/lib/vagrant-libvirt/action/create_domain.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/create_domain_volume.rb b/lib/vagrant-libvirt/action/create_domain_volume.rb new file mode 100644 index 0000000..021b47b --- /dev/null +++ b/lib/vagrant-libvirt/action/create_domain_volume.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/create_network_interfaces.rb b/lib/vagrant-libvirt/action/create_network_interfaces.rb new file mode 100644 index 0000000..d8b7f24 --- /dev/null +++ b/lib/vagrant-libvirt/action/create_network_interfaces.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/destroy_domain.rb b/lib/vagrant-libvirt/action/destroy_domain.rb new file mode 100644 index 0000000..e722d85 --- /dev/null +++ b/lib/vagrant-libvirt/action/destroy_domain.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/handle_box_image.rb b/lib/vagrant-libvirt/action/handle_box_image.rb new file mode 100644 index 0000000..a329560 --- /dev/null +++ b/lib/vagrant-libvirt/action/handle_box_image.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/handle_storage_pool.rb b/lib/vagrant-libvirt/action/handle_storage_pool.rb new file mode 100644 index 0000000..b088d31 --- /dev/null +++ b/lib/vagrant-libvirt/action/handle_storage_pool.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/is_created.rb b/lib/vagrant-libvirt/action/is_created.rb new file mode 100644 index 0000000..39760a0 --- /dev/null +++ b/lib/vagrant-libvirt/action/is_created.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/message_already_created.rb b/lib/vagrant-libvirt/action/message_already_created.rb new file mode 100644 index 0000000..a803d61 --- /dev/null +++ b/lib/vagrant-libvirt/action/message_already_created.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/message_not_created.rb b/lib/vagrant-libvirt/action/message_not_created.rb new file mode 100644 index 0000000..14dfc0e --- /dev/null +++ b/lib/vagrant-libvirt/action/message_not_created.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/read_ssh_info.rb b/lib/vagrant-libvirt/action/read_ssh_info.rb new file mode 100644 index 0000000..336ba96 --- /dev/null +++ b/lib/vagrant-libvirt/action/read_ssh_info.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/read_state.rb b/lib/vagrant-libvirt/action/read_state.rb new file mode 100644 index 0000000..f7543fa --- /dev/null +++ b/lib/vagrant-libvirt/action/read_state.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/set_name_of_domain.rb b/lib/vagrant-libvirt/action/set_name_of_domain.rb new file mode 100644 index 0000000..fa9c0ef --- /dev/null +++ b/lib/vagrant-libvirt/action/set_name_of_domain.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/start_domain.rb b/lib/vagrant-libvirt/action/start_domain.rb new file mode 100644 index 0000000..5ea9332 --- /dev/null +++ b/lib/vagrant-libvirt/action/start_domain.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/sync_folders.rb b/lib/vagrant-libvirt/action/sync_folders.rb new file mode 100644 index 0000000..f16fa02 --- /dev/null +++ b/lib/vagrant-libvirt/action/sync_folders.rb @@ -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 + diff --git a/lib/vagrant-libvirt/action/timed_provision.rb b/lib/vagrant-libvirt/action/timed_provision.rb new file mode 100644 index 0000000..b5cecd6 --- /dev/null +++ b/lib/vagrant-libvirt/action/timed_provision.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/wait_till_up.rb b/lib/vagrant-libvirt/action/wait_till_up.rb new file mode 100644 index 0000000..f42f901 --- /dev/null +++ b/lib/vagrant-libvirt/action/wait_till_up.rb @@ -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 + diff --git a/lib/vagrant-libvirt/config.rb b/lib/vagrant-libvirt/config.rb new file mode 100644 index 0000000..501effc --- /dev/null +++ b/lib/vagrant-libvirt/config.rb @@ -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 + diff --git a/lib/vagrant-libvirt/errors.rb b/lib/vagrant-libvirt/errors.rb new file mode 100644 index 0000000..b158dec --- /dev/null +++ b/lib/vagrant-libvirt/errors.rb @@ -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 diff --git a/lib/vagrant-libvirt/plugin.rb b/lib/vagrant-libvirt/plugin.rb new file mode 100644 index 0000000..fbe162c --- /dev/null +++ b/lib/vagrant-libvirt/plugin.rb @@ -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 + diff --git a/lib/vagrant-libvirt/provider.rb b/lib/vagrant-libvirt/provider.rb new file mode 100644 index 0000000..1b18aef --- /dev/null +++ b/lib/vagrant-libvirt/provider.rb @@ -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 + diff --git a/lib/vagrant-libvirt/templates/default_storage_pool.xml.erb b/lib/vagrant-libvirt/templates/default_storage_pool.xml.erb new file mode 100644 index 0000000..b63eea7 --- /dev/null +++ b/lib/vagrant-libvirt/templates/default_storage_pool.xml.erb @@ -0,0 +1,13 @@ + + default + + + + /var/lib/libvirt/images + + 0755 + -1 + -1 + + + diff --git a/lib/vagrant-libvirt/templates/domain.xml.erb b/lib/vagrant-libvirt/templates/domain.xml.erb new file mode 100644 index 0000000..d1eb492 --- /dev/null +++ b/lib/vagrant-libvirt/templates/domain.xml.erb @@ -0,0 +1,34 @@ + + <%= @name %> + <%= @memory_size %> + <%= @cpus %> + + hvm + + + + + + + + + + + + + <%# we need to ensure a unique target dev -%> + + + + + + + + + + + + + diff --git a/lib/vagrant-libvirt/templates/interface.xml.erb b/lib/vagrant-libvirt/templates/interface.xml.erb new file mode 100644 index 0000000..39edc0f --- /dev/null +++ b/lib/vagrant-libvirt/templates/interface.xml.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib/vagrant-libvirt/templates/volume_snapshot.xml.erb b/lib/vagrant-libvirt/templates/volume_snapshot.xml.erb new file mode 100644 index 0000000..304d248 --- /dev/null +++ b/lib/vagrant-libvirt/templates/volume_snapshot.xml.erb @@ -0,0 +1,26 @@ + + <%= @name %> + <%= @capacity %> + + + + + 0 + 0 + 0600 + + + + + + <%= @backing_file %> + + + 0 + 0 + 0600 + + + + + diff --git a/lib/vagrant-libvirt/util.rb b/lib/vagrant-libvirt/util.rb new file mode 100644 index 0000000..870fea4 --- /dev/null +++ b/lib/vagrant-libvirt/util.rb @@ -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 + diff --git a/lib/vagrant-libvirt/util/collection.rb b/lib/vagrant-libvirt/util/collection.rb new file mode 100644 index 0000000..89d901e --- /dev/null +++ b/lib/vagrant-libvirt/util/collection.rb @@ -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 + diff --git a/lib/vagrant-libvirt/util/erb_template.rb b/lib/vagrant-libvirt/util/erb_template.rb new file mode 100644 index 0000000..4f2b4be --- /dev/null +++ b/lib/vagrant-libvirt/util/erb_template.rb @@ -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 + diff --git a/lib/vagrant-libvirt/util/timer.rb b/lib/vagrant-libvirt/util/timer.rb new file mode 100644 index 0000000..6349d49 --- /dev/null +++ b/lib/vagrant-libvirt/util/timer.rb @@ -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 diff --git a/lib/vagrant-libvirt/version.rb b/lib/vagrant-libvirt/version.rb new file mode 100644 index 0000000..5e8b1a3 --- /dev/null +++ b/lib/vagrant-libvirt/version.rb @@ -0,0 +1,5 @@ +module VagrantPlugins + module Libvirt + VERSION = "0.0.1" + end +end diff --git a/locales/en.yml b/locales/en.yml new file mode 100644 index 0000000..9e999cd --- /dev/null +++ b/locales/en.yml @@ -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`. diff --git a/vagrant-libvirt.gemspec b/vagrant-libvirt.gemspec new file mode 100644 index 0000000..ae4bba7 --- /dev/null +++ b/vagrant-libvirt.gemspec @@ -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 +