boxgrinder-build 0.9.8 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +15 -0
- data/Manifest +7 -22
- data/Rakefile +3 -1
- data/bin/boxgrinder-build +1 -1
- data/boxgrinder-build.gemspec +17 -13
- data/integ/appliances/jeos-centos6.appl +4 -0
- data/lib/boxgrinder-build/appliance.rb +61 -23
- data/lib/boxgrinder-build/helpers/ec2-helper.rb +18 -0
- data/lib/boxgrinder-build/helpers/linux-helper.rb +41 -2
- data/lib/boxgrinder-build/helpers/plugin-helper.rb +3 -0
- data/lib/boxgrinder-build/helpers/s3-helper.rb +18 -0
- data/lib/boxgrinder-build/helpers/sftp-helper.rb +124 -0
- data/lib/boxgrinder-build/managers/plugin-manager.rb +5 -3
- data/lib/boxgrinder-build/plugins/base-plugin.rb +2 -22
- data/lib/boxgrinder-build/plugins/delivery/ebs/ebs-plugin.rb +26 -15
- data/lib/boxgrinder-build/plugins/delivery/elastichosts/elastichosts-plugin.rb +2 -1
- data/lib/boxgrinder-build/plugins/delivery/libvirt/libvirt-capabilities.rb +164 -0
- data/lib/boxgrinder-build/plugins/delivery/libvirt/libvirt-plugin.rb +313 -0
- data/lib/boxgrinder-build/plugins/delivery/local/local-plugin.rb +2 -1
- data/lib/boxgrinder-build/plugins/delivery/openstack/openstack-plugin.rb +133 -0
- data/lib/boxgrinder-build/plugins/delivery/s3/s3-plugin.rb +15 -2
- data/lib/boxgrinder-build/plugins/delivery/sftp/sftp-plugin.rb +20 -106
- data/lib/boxgrinder-build/plugins/os/centos/centos-plugin.rb +3 -3
- data/lib/boxgrinder-build/plugins/os/fedora/fedora-plugin.rb +2 -1
- data/lib/boxgrinder-build/plugins/os/rhel/rhel-plugin.rb +2 -1
- data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-based-os-plugin.rb +6 -64
- data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-dependency-validator.rb +2 -1
- data/lib/boxgrinder-build/plugins/os/sl/sl-plugin.rb +1 -2
- data/lib/boxgrinder-build/plugins/platform/ec2/ec2-plugin.rb +15 -1
- data/lib/boxgrinder-build/plugins/platform/virtualbox/virtualbox-plugin.rb +2 -1
- data/lib/boxgrinder-build/plugins/platform/virtualpc/virtualpc-plugin.rb +58 -0
- data/lib/boxgrinder-build/plugins/platform/vmware/vmware-plugin.rb +2 -1
- data/rubygem-boxgrinder-build.spec +25 -1
- data/spec/appliance-spec.rb +1 -58
- data/spec/helpers/linux-helper-spec.rb +70 -0
- data/spec/managers/plugin-manager-spec.rb +4 -13
- data/spec/plugins/delivery/ebs/ebs-plugin-spec.rb +6 -14
- data/spec/plugins/delivery/elastichosts/elastichosts-plugin-spec.rb +5 -6
- data/spec/plugins/delivery/libvirt/libvirt-plugin-spec.rb +300 -0
- data/spec/plugins/delivery/libvirt/libvirt_modified.xml +25 -0
- data/spec/plugins/delivery/libvirt/libvirt_modify.sh +18 -0
- data/spec/plugins/delivery/libvirt/libvirt_test.xml +24 -0
- data/spec/plugins/delivery/local/local-plugin-spec.rb +3 -6
- data/spec/plugins/delivery/openstack/openstack-plugin-spec.rb +103 -0
- data/spec/plugins/delivery/s3/s3-plugin-spec.rb +16 -5
- data/spec/plugins/os/rpm-based/rpm-based-os-plugin-spec.rb +2 -80
- data/spec/plugins/platform/ec2/ec2-plugin-spec.rb +15 -1
- data/spec/plugins/platform/virtualbox/virtualbox-plugin-spec.rb +6 -6
- data/spec/plugins/platform/virtualpc/virtualpc-plugin-spec.rb +90 -0
- data/spec/plugins/platform/vmware/vmware-plugin-spec.rb +5 -9
- data/spec/rspec-plugin-helper.rb +47 -0
- metadata +54 -10
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'boxgrinder-core/helpers/log-helper'
|
2
|
+
require 'net/ssh'
|
3
|
+
require 'net/sftp'
|
4
|
+
|
5
|
+
module BoxGrinder
|
6
|
+
class SFTPHelper
|
7
|
+
def initialize(options={})
|
8
|
+
@log = options[:log] || LogHelper.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def connect(host, username, options={})
|
12
|
+
@log.info "Connecting to #{host}..."
|
13
|
+
@ssh = Net::SSH.start(host, username, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def connected?
|
17
|
+
return true if !@ssh.nil? and !@ssh.closed?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def disconnect
|
22
|
+
@log.info "Disconnecting from host..."
|
23
|
+
@ssh.close if connected?
|
24
|
+
@ssh = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def upload_files(path, default_permissions, overwrite, files = {})
|
28
|
+
return if files.size == 0
|
29
|
+
|
30
|
+
raise "You're not connected to server" unless connected?
|
31
|
+
|
32
|
+
@log.debug "Files to upload:"
|
33
|
+
|
34
|
+
files.each do |remote, local|
|
35
|
+
@log.debug "#{File.basename(local)} => #{path}/#{remote}"
|
36
|
+
end
|
37
|
+
|
38
|
+
global_size = 0
|
39
|
+
|
40
|
+
files.each_value do |file|
|
41
|
+
global_size += File.size(file)
|
42
|
+
end
|
43
|
+
|
44
|
+
global_size_kb = global_size / 1024
|
45
|
+
global_size_mb = global_size_kb / 1024
|
46
|
+
|
47
|
+
@log.info "#{files.size} files to upload (#{global_size_mb > 0 ? global_size_mb.to_s + "MB" : global_size_kb > 0 ? global_size_kb.to_s + "kB" : global_size.to_s})"
|
48
|
+
|
49
|
+
@ssh.sftp.connect do |sftp|
|
50
|
+
begin
|
51
|
+
sftp.stat!(path)
|
52
|
+
rescue Net::SFTP::StatusException => e
|
53
|
+
raise unless e.code == 2
|
54
|
+
@ssh.exec!("mkdir -p #{path}")
|
55
|
+
end
|
56
|
+
|
57
|
+
nb = 0
|
58
|
+
|
59
|
+
files.each do |key, local|
|
60
|
+
name = File.basename(local)
|
61
|
+
remote = "#{path}/#{key}"
|
62
|
+
size_b = File.size(local)
|
63
|
+
size_kb = size_b / 1024
|
64
|
+
nb_of = "#{nb += 1}/#{files.size}"
|
65
|
+
|
66
|
+
begin
|
67
|
+
sftp.stat!(remote)
|
68
|
+
|
69
|
+
unless overwrite
|
70
|
+
|
71
|
+
local_md5_sum = `md5sum #{local} | awk '{ print $1 }'`.strip
|
72
|
+
remote_md5_sum = @ssh.exec!("md5sum #{remote} | awk '{ print $1 }'").strip
|
73
|
+
|
74
|
+
if (local_md5_sum.eql?(remote_md5_sum))
|
75
|
+
@log.info "#{nb_of} #{name}: files are identical (md5sum: #{local_md5_sum}), skipping..."
|
76
|
+
next
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
rescue Net::SFTP::StatusException => e
|
81
|
+
raise unless e.code == 2
|
82
|
+
end
|
83
|
+
|
84
|
+
@ssh.exec!("mkdir -p #{File.dirname(remote) }")
|
85
|
+
|
86
|
+
pbar = ProgressBar.new("#{nb_of} #{name}", size_b)
|
87
|
+
pbar.file_transfer_mode
|
88
|
+
|
89
|
+
sftp.upload!(local, remote) do |event, uploader, * args|
|
90
|
+
case event
|
91
|
+
when :open then
|
92
|
+
when :put then
|
93
|
+
pbar.set(args[1])
|
94
|
+
when :close then
|
95
|
+
when :mkdir then
|
96
|
+
when :finish then
|
97
|
+
pbar.finish
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
sftp.setstat(remote, :permissions => default_permissions)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
## Extend the default baked paths in net-ssh/net-sftp to include SUDO_USER
|
107
|
+
## and/or LOGNAME key directories too.
|
108
|
+
#def generate_keypaths
|
109
|
+
# keys = %w(id_rsa id_dsa)
|
110
|
+
# dirs = %w(.ssh .ssh2)
|
111
|
+
# paths = %w(~/.ssh/id_rsa ~/.ssh/id_dsa ~/.ssh2/id_rsa ~/.ssh2/id_dsa)
|
112
|
+
# ['SUDO_USER','LOGNAME'].inject(paths) do |accum, var|
|
113
|
+
# if user = ENV[var]
|
114
|
+
# accum << dirs.collect do |d|
|
115
|
+
# keys.collect { |k| File.expand_path("~#{user}/#{d}/#{k}") if File.exist?("~#{user}/#{d}/#{k}") }
|
116
|
+
# end.flatten!
|
117
|
+
# end
|
118
|
+
# accum
|
119
|
+
# end.flatten!
|
120
|
+
# paths
|
121
|
+
#end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -20,8 +20,8 @@ require 'singleton'
|
|
20
20
|
|
21
21
|
module BoxGrinder
|
22
22
|
module Plugins
|
23
|
-
def plugin(
|
24
|
-
PluginManager.instance.register_plugin(
|
23
|
+
def plugin(info)
|
24
|
+
PluginManager.instance.register_plugin(self, info)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -37,7 +37,9 @@ module BoxGrinder
|
|
37
37
|
@plugins = {:delivery => {}, :os => {}, :platform => {}}
|
38
38
|
end
|
39
39
|
|
40
|
-
def register_plugin(info)
|
40
|
+
def register_plugin(clazz, info)
|
41
|
+
info.merge!(:class => clazz)
|
42
|
+
|
41
43
|
validate_plugin_info(info)
|
42
44
|
|
43
45
|
raise "We already have registered plugin for #{info[:name]}." unless @plugins[info[:name]].nil?
|
@@ -29,6 +29,8 @@ require 'logger'
|
|
29
29
|
|
30
30
|
module BoxGrinder
|
31
31
|
class BasePlugin
|
32
|
+
include Plugins
|
33
|
+
|
32
34
|
attr_reader :plugin_info
|
33
35
|
attr_reader :deliverables
|
34
36
|
|
@@ -73,34 +75,12 @@ module BoxGrinder
|
|
73
75
|
# TODO Needs some thoughts - if we don't have deliverables that we care about - should they be in @deliverables?
|
74
76
|
@move_deliverables = true
|
75
77
|
|
76
|
-
# Validate the plugin configuration.
|
77
|
-
# Please make the validate method as simple as possible, because it'll be executed also in unit tests.
|
78
|
-
validate
|
79
|
-
|
80
78
|
# The plugin is initialized now. We can do some fancy stuff with it.
|
81
79
|
@initialized = true
|
82
80
|
|
83
|
-
# If there is something defined in the plugin that should be executed after plugin initialization - it should go
|
84
|
-
# to after_init method.
|
85
|
-
after_init
|
86
|
-
|
87
81
|
self
|
88
82
|
end
|
89
83
|
|
90
|
-
# This is a stub that should be overriden by the actual plugin implementation.
|
91
|
-
# It can use subtype(:TYPE) calls to validate for a specific type.
|
92
|
-
# KISS!
|
93
|
-
def validate
|
94
|
-
end
|
95
|
-
|
96
|
-
# Callback - executed after initialization.
|
97
|
-
def after_init
|
98
|
-
end
|
99
|
-
|
100
|
-
# Callback - executed after execution.
|
101
|
-
def after_execute
|
102
|
-
end
|
103
|
-
|
104
84
|
# Validation helper method.
|
105
85
|
#
|
106
86
|
# Execute the validation only for selected plugin type.
|
@@ -32,6 +32,8 @@ module BoxGrinder
|
|
32
32
|
TIMEOUT = 1000 #seconds
|
33
33
|
EC2_HOSTNAME_LOOKUP_TIMEOUT = 10
|
34
34
|
|
35
|
+
plugin :type => :delivery, :name => :ebs, :full_name => "Elastic Block Storage"
|
36
|
+
|
35
37
|
def validate
|
36
38
|
@ec2_endpoints = EC2Helper::endpoints
|
37
39
|
|
@@ -41,6 +43,9 @@ module BoxGrinder
|
|
41
43
|
@current_instance_id = EC2Helper::current_instance_id
|
42
44
|
@current_region = EC2Helper::availability_zone_to_region(@current_availability_zone)
|
43
45
|
|
46
|
+
set_default_config_value('kernel', false)
|
47
|
+
set_default_config_value('ramdisk', false)
|
48
|
+
|
44
49
|
set_default_config_value('availability_zone', @current_availability_zone)
|
45
50
|
set_default_config_value('delete_on_termination', true)
|
46
51
|
set_default_config_value('overwrite', false)
|
@@ -145,20 +150,27 @@ module BoxGrinder
|
|
145
150
|
volume.delete
|
146
151
|
|
147
152
|
@log.info "Registering image..."
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
153
|
+
|
154
|
+
optmap = {
|
155
|
+
:name => ebs_appliance_name,
|
156
|
+
:root_device_name => ROOT_DEVICE_NAME,
|
157
|
+
:block_device_mappings => { ROOT_DEVICE_NAME => {
|
158
|
+
:snapshot => snapshot,
|
159
|
+
:delete_on_termination => @plugin_config['delete_on_termination']
|
160
|
+
},
|
161
|
+
'/dev/sdb' => 'ephemeral0',
|
162
|
+
'/dev/sdc' => 'ephemeral1',
|
163
|
+
'/dev/sdd' => 'ephemeral2',
|
164
|
+
'/dev/sde' => 'ephemeral3'},
|
165
|
+
:architecture => @appliance_config.hardware.base_arch,
|
166
|
+
:kernel_id => @plugin_config['kernel'] || @ec2_endpoints[@current_region][:kernel][@appliance_config.hardware.base_arch.intern][:aki],
|
167
|
+
:description => ebs_appliance_description
|
168
|
+
}
|
169
|
+
|
170
|
+
optmap.merge!(:ramdisk_id => @plugin_config['ramdisk']) if @plugin_config['ramdisk']
|
171
|
+
|
172
|
+
|
173
|
+
image = @ec2.images.create(optmap)
|
162
174
|
|
163
175
|
@log.info "Waiting for the new EBS AMI to become available"
|
164
176
|
@ec2helper.wait_for_image_state(:available, image)
|
@@ -258,4 +270,3 @@ module BoxGrinder
|
|
258
270
|
end
|
259
271
|
end
|
260
272
|
|
261
|
-
plugin :class => BoxGrinder::EBSPlugin, :type => :delivery, :name => :ebs, :full_name => "Elastic Block Storage"
|
@@ -24,6 +24,8 @@ require 'cgi'
|
|
24
24
|
|
25
25
|
module BoxGrinder
|
26
26
|
class ElasticHostsPlugin < BasePlugin
|
27
|
+
plugin :type => :delivery, :name => :elastichosts, :full_name => "ElasticHosts"
|
28
|
+
|
27
29
|
def validate
|
28
30
|
set_default_config_value('chunk', 64) # chunk size in MB
|
29
31
|
set_default_config_value('start_part', 0) # part number to start uploading
|
@@ -209,4 +211,3 @@ module BoxGrinder
|
|
209
211
|
end
|
210
212
|
end
|
211
213
|
|
212
|
-
plugin :class => BoxGrinder::ElasticHostsPlugin, :type => :delivery, :name => :elastichosts, :full_name => "ElasticHosts"
|
@@ -0,0 +1,164 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2010 Red Hat, Inc.
|
3
|
+
#
|
4
|
+
# This is free software; you can redistribute it and/or modify it
|
5
|
+
# under the terms of the GNU Lesser General Public License as
|
6
|
+
# published by the Free Software Foundation; either version 3 of
|
7
|
+
# the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This software is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
12
|
+
# Lesser General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU Lesser General Public
|
15
|
+
# License along with this software; if not, write to the Free
|
16
|
+
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
17
|
+
# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
18
|
+
|
19
|
+
require 'libvirt'
|
20
|
+
require 'enumerator'
|
21
|
+
require 'nokogiri'
|
22
|
+
require 'ostruct'
|
23
|
+
|
24
|
+
module BoxGrinder
|
25
|
+
class LibvirtCapabilities
|
26
|
+
|
27
|
+
class Domain
|
28
|
+
include Comparable
|
29
|
+
attr_accessor :name, :bus, :virt_rank, :virt_map
|
30
|
+
def initialize(name, bus, virt_rank)
|
31
|
+
@name = name
|
32
|
+
@bus = bus
|
33
|
+
@virt_rank = virt_rank.freeze
|
34
|
+
@virt_map = virt_rank.enum_for(:each_with_index).inject({}) do |accum, (virt, rank)|
|
35
|
+
accum.merge(virt => rank)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def <=>(other)
|
40
|
+
self.name <=> other.name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Plugin
|
45
|
+
include Comparable
|
46
|
+
attr_accessor :name, :domain_rank, :domain_map
|
47
|
+
def initialize(name, domain_rank)
|
48
|
+
@name = name
|
49
|
+
@domain_map = domain_rank.enum_for(:each_with_index).inject({}) do |accum, (domain, rank)|
|
50
|
+
accum.merge(domain.name => {:domain => domain, :rank => rank})
|
51
|
+
end
|
52
|
+
@domain_map.freeze
|
53
|
+
@domain_rank = domain_rank.freeze
|
54
|
+
end
|
55
|
+
|
56
|
+
def <=>(other)
|
57
|
+
self.name <=> other.name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Arrays are populated in order of precedence. Best first.
|
62
|
+
DEFAULT_DOMAIN_MAPPINGS = {
|
63
|
+
:xen => { :bus => :xen, :virt_rank => [:xen, :linux, :hvm] },
|
64
|
+
:kqemu => { :bus => :virtio, :virt_rank => [:hvm] },
|
65
|
+
:kvm => { :bus => :virtio, :virt_rank => [:xen, :linux, :hvm] },
|
66
|
+
:qemu => { :bus => :ide, :virt_rank => [:xen, :linux, :hvm] },
|
67
|
+
:vbox => { :bus => :virtio, :virt_rank => [:hvm] },
|
68
|
+
:vmware => { :bus => :ide, :virt_rank => [:hvm] }
|
69
|
+
}
|
70
|
+
|
71
|
+
PLUGIN_MAPPINGS = {
|
72
|
+
:default => { :domain_rank => [:kvm, :xen, :kqemu, :qemu] },
|
73
|
+
:virtualbox => { :domain_rank => [:vbox] },
|
74
|
+
:xen => { :domain_rank => [:xen] },
|
75
|
+
:citrix => { :domain_rank => [:xen] },
|
76
|
+
:kvm => { :domain_rank => [:kvm] },
|
77
|
+
:vmware => { :domain_rank => [:vmware] },
|
78
|
+
:ec2 => { :domain_rank => [:xen, :qemu] }
|
79
|
+
}
|
80
|
+
|
81
|
+
DOMAINS = DEFAULT_DOMAIN_MAPPINGS.inject({}) do |accum, mapping|
|
82
|
+
accum.merge(mapping.first => Domain.new(mapping.first, mapping.last[:bus], mapping.last[:virt_rank]))
|
83
|
+
end
|
84
|
+
|
85
|
+
PLUGINS = PLUGIN_MAPPINGS.inject({}) do |accum, mapping|
|
86
|
+
d_refs = mapping.last[:domain_rank].collect{|d| DOMAINS[d]}
|
87
|
+
accum.merge(mapping.first => Plugin.new(mapping.first, d_refs))
|
88
|
+
end
|
89
|
+
|
90
|
+
def initialize(opts={})
|
91
|
+
@log = opts[:log] || LogHelper.new
|
92
|
+
end
|
93
|
+
|
94
|
+
# Connect to the remote machine and determine the best available settings
|
95
|
+
def determine_capabilities(conn, previous_plugin_info)
|
96
|
+
plugin = get_plugin(previous_plugin_info)
|
97
|
+
root = Nokogiri::XML.parse(conn.capabilities)
|
98
|
+
guests = root.xpath("//guest/arch[@name='x86_64']/..")
|
99
|
+
|
100
|
+
guests = guests.sort do |a, b|
|
101
|
+
dom_maps = [a,b].map { |x| plugin.domain_map[xpath_first_intern(x, './/domain/@type')] }
|
102
|
+
|
103
|
+
# Handle unknown mappings
|
104
|
+
next resolve_unknowns(dom_maps) if dom_maps.include?(nil)
|
105
|
+
|
106
|
+
# Compare according to domain ranking
|
107
|
+
dom_rank = dom_maps.map { |m| m[:rank]}.reduce(:<=>)
|
108
|
+
|
109
|
+
# Compare according to virtualisation ranking
|
110
|
+
virt_rank = [a,b].enum_for(:each_with_index).map do |x, i|
|
111
|
+
dom_maps[i][:domain].virt_map[xpath_first_intern(x, './/os_type')]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Handle unknown mappings
|
115
|
+
next resolve_unknowns(virt_rank) if virt_rank.include?(nil)
|
116
|
+
|
117
|
+
# Domain rank first
|
118
|
+
next dom_rank unless dom_rank == 0
|
119
|
+
|
120
|
+
# OS type rank second
|
121
|
+
virt_rank.reduce(:<=>)
|
122
|
+
end
|
123
|
+
# Favourite!
|
124
|
+
build_guest(guests.first)
|
125
|
+
end
|
126
|
+
|
127
|
+
def resolve_unknowns(pair)
|
128
|
+
return 0 if pair.first.nil? and pair.last.nil?
|
129
|
+
return 1 if pair.first.nil?
|
130
|
+
-1 if pair.last.nil?
|
131
|
+
end
|
132
|
+
|
133
|
+
def build_guest(xml)
|
134
|
+
dom = DOMAINS[xpath_first_intern(xml, ".//domain/@type")]
|
135
|
+
bus = 'ide'
|
136
|
+
bus = dom.bus if dom
|
137
|
+
|
138
|
+
OpenStruct.new({
|
139
|
+
:domain_type => xpath_first_intern(xml, ".//domain/@type"),
|
140
|
+
:os_type => xpath_first_intern(xml, './/os_type'),
|
141
|
+
:bus => bus
|
142
|
+
})
|
143
|
+
end
|
144
|
+
|
145
|
+
def xpath_first_intern(xml, path)
|
146
|
+
xml.xpath(path).first.text.intern
|
147
|
+
end
|
148
|
+
|
149
|
+
# At present we don't have enough meta-data to work with to easily generalise,
|
150
|
+
# so we have to assume defaults often. This is something to improve later.
|
151
|
+
def get_plugin(previous_plugin_info)
|
152
|
+
if previous_plugin_info[:type] == :platform
|
153
|
+
if PLUGINS.has_key?(previous_plugin_info[:name])
|
154
|
+
@log.debug("Using #{previous_plugin_info[:name]} mapping")
|
155
|
+
return PLUGINS[previous_plugin_info[:name]]
|
156
|
+
else
|
157
|
+
@log.debug("This plugin does not know what mappings to choose, so will assume default values where user values are not provided.")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
@log.debug("Using default domain mappings.")
|
161
|
+
PLUGINS[:default]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,313 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2010 Red Hat, Inc.
|
3
|
+
#
|
4
|
+
# This is free software; you can redistribute it and/or modify it
|
5
|
+
# under the terms of the GNU Lesser General Public License as
|
6
|
+
# published by the Free Software Foundation; either version 3 of
|
7
|
+
# the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This software is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
12
|
+
# Lesser General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU Lesser General Public
|
15
|
+
# License along with this software; if not, write to the Free
|
16
|
+
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
17
|
+
# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
18
|
+
|
19
|
+
require 'boxgrinder-build/plugins/base-plugin'
|
20
|
+
require 'boxgrinder-build/plugins/delivery/libvirt/libvirt-capabilities'
|
21
|
+
require 'boxgrinder-build/helpers/sftp-helper'
|
22
|
+
|
23
|
+
require 'libvirt'
|
24
|
+
require 'net/sftp'
|
25
|
+
require 'fileutils'
|
26
|
+
require 'uri'
|
27
|
+
require 'etc'
|
28
|
+
require 'builder'
|
29
|
+
require 'ostruct'
|
30
|
+
|
31
|
+
module BoxGrinder
|
32
|
+
|
33
|
+
# @plugin_config [String] connection_uri Libvirt endpoint address. If you are
|
34
|
+
# using authenticated transport such as +ssh+ you should register your keys with
|
35
|
+
# an ssh agent. See: {http://libvirt.org/uri.html Libvirt Connection URIs}.
|
36
|
+
# * Default: +empty string+
|
37
|
+
# * Examples: <tt>qemu+ssh://user@example.com/system</tt>
|
38
|
+
# * +qemu:///system+
|
39
|
+
#
|
40
|
+
# @plugin_config [String] image_delivery_uri Where to deliver the image to. This must be a
|
41
|
+
# local path or an SFTP address. The local ssh-agent is used for keys if available.
|
42
|
+
# * Default: +/var/lib/libvirt/images+
|
43
|
+
# * Examples: +sftp\://user@example.com/some/path+
|
44
|
+
# * +sftp\://user:pass@example.com/some/path+ It is advisable to use keys with ssh-agent.
|
45
|
+
#
|
46
|
+
# @plugin_config [String] libvirt_image_uri Where the image will be on the Libvirt machine.
|
47
|
+
# * Default: +image_delivery_uri+ _path_ element.
|
48
|
+
# * Example: +/var/lib/libvirt/images+
|
49
|
+
#
|
50
|
+
# @plugin_config [Int] default_permissions Permissions of delivered image. Examples:
|
51
|
+
# * Default: +0770+
|
52
|
+
# * Examples: +0755+, +0775+
|
53
|
+
#
|
54
|
+
# @plugin_config [Int] overwrite Overwrite any identically named file at the delivery path.
|
55
|
+
# Also undefines any existing domain of the same name.
|
56
|
+
# * Default: +false+
|
57
|
+
#
|
58
|
+
# @plugin_config [String] script Path to user provided script to modify XML before registration
|
59
|
+
# with Libvirt. Plugin passes the raw XML, and consumes stdout to use as revised XML document.
|
60
|
+
#
|
61
|
+
# @plugin_config [Bool] remote_no_verify Disable certificate verification procedures
|
62
|
+
# * Default: +true+
|
63
|
+
#
|
64
|
+
# @plugin_config [Bool] xml_only Do not connect to the Libvirt hypervisor, just assume sensible
|
65
|
+
# defaults where no user values are provided, and produce the XML domain.
|
66
|
+
# * Default: +false+
|
67
|
+
#
|
68
|
+
# @plugin_config [String] appliance_name Name for the appliance to be registered as in Libvirt.
|
69
|
+
# At present the user can only specify literal strings.
|
70
|
+
# * Default: +name-version-release-os_name-os_version-arch-platform+
|
71
|
+
# * Example: +boxgrinder-f16-rocks+
|
72
|
+
#
|
73
|
+
# @plugin_config [String] domain_type Libvirt domain type.
|
74
|
+
# * Default is a calculated value. Unless you are using +xml_only+ the remote instance will
|
75
|
+
# be contacted and an attempt to determine the best value will be made. If +xml_only+
|
76
|
+
# is set then a safe pre-determined default is used. User-set values take precedence.
|
77
|
+
# See _type_: {http://libvirt.org/formatdomain.html#elements Domain format}
|
78
|
+
# * Examples: +qemu+, +kvm+, +xen+
|
79
|
+
#
|
80
|
+
# @plugin_config [String] virt_type Libvirt virt type.
|
81
|
+
# * Default is a calculated value. Where available paravirtual is preferred.
|
82
|
+
# See _type_: {http://libvirt.org/formatdomain.html#elementsOSBIOS BIOS bootloader}.
|
83
|
+
# * Examples: +hvm+, +xen+, +linux+
|
84
|
+
#
|
85
|
+
# @plugin_config [String] bus Disk bus.
|
86
|
+
# * Default is a pre-determined value depending on the domain type. User-set values take
|
87
|
+
# precedence
|
88
|
+
# * Examples: +virtio+, +ide+
|
89
|
+
#
|
90
|
+
# @plugin_config [String] network Network name. If you require a more complex setup
|
91
|
+
# than a simple network name, then you should create and set a +script+.
|
92
|
+
# * Default: +default+
|
93
|
+
class LibvirtPlugin < BasePlugin
|
94
|
+
|
95
|
+
plugin :type => :delivery, :name => :libvirt, :full_name => "libvirt Virtualisation API"
|
96
|
+
|
97
|
+
def set_defaults
|
98
|
+
set_default_config_value('connection_uri', '')
|
99
|
+
set_default_config_value('script', false)
|
100
|
+
set_default_config_value('image_delivery_uri', '/var/lib/libvirt/images')
|
101
|
+
set_default_config_value('libvirt_image_uri', false)
|
102
|
+
set_default_config_value('remote_no_verify', true)
|
103
|
+
set_default_config_value('overwrite', false)
|
104
|
+
set_default_config_value('default_permissions', 0770)
|
105
|
+
set_default_config_value('xml_only', false)
|
106
|
+
# Manual overrides
|
107
|
+
set_default_config_value('appliance_name', [@appliance_config.name, @appliance_config.version, @appliance_config.release,
|
108
|
+
@appliance_config.os.name, @appliance_config.os.version, @appliance_config.hardware.arch,
|
109
|
+
current_platform].join("-"))
|
110
|
+
set_default_config_value('domain_type', false)
|
111
|
+
set_default_config_value('virt_type', false)
|
112
|
+
set_default_config_value('bus', false)
|
113
|
+
set_default_config_value('network', 'default')
|
114
|
+
set_default_config_value('mac', false)
|
115
|
+
set_default_config_value('noautoconsole', false)
|
116
|
+
|
117
|
+
libvirt_code_patch
|
118
|
+
end
|
119
|
+
|
120
|
+
def validate
|
121
|
+
set_defaults
|
122
|
+
|
123
|
+
['connection_uri', 'xml_only', 'network', 'domain_type', 'virt_type', 'script',
|
124
|
+
'bus', 'appliance_name', 'default_permissions', 'overwrite', 'noautoconsole',
|
125
|
+
'mac'].each do |v|
|
126
|
+
self.instance_variable_set(:"@#{v}", @plugin_config[v])
|
127
|
+
end
|
128
|
+
|
129
|
+
@libvirt_capabilities = LibvirtCapabilities.new(:log => @log)
|
130
|
+
@image_delivery_uri = URI.parse(@plugin_config['image_delivery_uri'])
|
131
|
+
@libvirt_image_uri = (@plugin_config['libvirt_image_uri'] || @image_delivery_uri.path)
|
132
|
+
|
133
|
+
@remote_no_verify = @plugin_config['remote_no_verify'] ? 1 : 0
|
134
|
+
|
135
|
+
(@connection_uri.include?('?') ? '&' : '?') + "no_verify=#{@remote_no_verify}"
|
136
|
+
@connection_uri = URI.parse(@plugin_config['connection_uri'])
|
137
|
+
end
|
138
|
+
|
139
|
+
def execute
|
140
|
+
if @image_delivery_uri.scheme =~ /sftp/
|
141
|
+
@log.info("Transferring file via SFTP...")
|
142
|
+
upload_image
|
143
|
+
else
|
144
|
+
@log.info("Copying disk #{@previous_deliverables.disk} to: #{@image_delivery_uri.path}...")
|
145
|
+
FileUtils.cp(@previous_deliverables.disk, @image_delivery_uri.path)
|
146
|
+
end
|
147
|
+
|
148
|
+
if @xml_only
|
149
|
+
@log.info("Determining locally only.")
|
150
|
+
xml = determine_locally
|
151
|
+
else
|
152
|
+
@log.info("Determining remotely.")
|
153
|
+
xml = determine_remotely
|
154
|
+
end
|
155
|
+
write_xml(xml)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Interact with a libvirtd, attempt to determine optimal settings where possible.
|
159
|
+
# Register the appliance as a new domain.
|
160
|
+
def determine_remotely
|
161
|
+
# Remove password field from URI, as libvirt doesn't support it directly. We can use it for passphrase if needed.
|
162
|
+
lv_uri = URI::Generic.build(:scheme => @connection_uri.scheme, :userinfo => @connection_uri.user,
|
163
|
+
:host => @connection_uri.host, :path => @connection_uri.path,
|
164
|
+
:query => @connection_uri.query)
|
165
|
+
|
166
|
+
# The authentication only pertains to libvirtd itself and _not_ the transport (e.g. SSH).
|
167
|
+
conn = Libvirt::open_auth(lv_uri.to_s, [Libvirt::CRED_AUTHNAME, Libvirt::CRED_PASSPHRASE]) do |cred|
|
168
|
+
case cred["type"]
|
169
|
+
when Libvirt::CRED_AUTHNAME
|
170
|
+
@connection_uri.user
|
171
|
+
when Libvirt::CRED_PASSPHRASE
|
172
|
+
@connection_uri.password
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
if dom = get_existing_domain(conn, @appliance_name)
|
177
|
+
unless @overwrite
|
178
|
+
@log.fatal("A domain already exists with the name #{@appliance_name}. Set overwrite:true to automatically destroy and undefine it.")
|
179
|
+
raise RuntimeError, "Domain '#{@appliance_name}' already exists" #Make better specific exception
|
180
|
+
end
|
181
|
+
@log.info("Undefining existing domain #{@appliance_name}")
|
182
|
+
undefine_domain(dom)
|
183
|
+
end
|
184
|
+
|
185
|
+
guest = @libvirt_capabilities.determine_capabilities(conn, @previous_plugin_info)
|
186
|
+
|
187
|
+
raise "Remote libvirt machine offered no viable guests!" if guest.nil?
|
188
|
+
|
189
|
+
xml = generate_xml(guest)
|
190
|
+
@log.info("Defining domain #{@appliance_name}")
|
191
|
+
conn.define_domain_xml(xml)
|
192
|
+
xml
|
193
|
+
ensure
|
194
|
+
if conn
|
195
|
+
conn.close unless conn.closed?
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Make no external connections, just dump a basic XML skeleton and provide sensible defaults
|
200
|
+
# where user provided values are not given.
|
201
|
+
def determine_locally
|
202
|
+
domain = @libvirt_capabilities.get_plugin(@previous_plugin_info).domain_rank.last
|
203
|
+
generate_xml(OpenStruct.new({
|
204
|
+
:domain_type => domain.name,
|
205
|
+
:os_type => domain.virt_rank.last,
|
206
|
+
:bus => domain.bus
|
207
|
+
}))
|
208
|
+
end
|
209
|
+
|
210
|
+
# Upload an image via SFTP
|
211
|
+
def upload_image
|
212
|
+
uploader = SFTPHelper.new(:log => @log)
|
213
|
+
|
214
|
+
#SFTP library automagically uses keys registered with the OS first before trying a password.
|
215
|
+
uploader.connect(@image_delivery_uri.host,
|
216
|
+
(@image_delivery_uri.user || Etc.getlogin),
|
217
|
+
:password => @image_delivery_uri.password)
|
218
|
+
|
219
|
+
uploader.upload_files(@image_delivery_uri.path,
|
220
|
+
@default_permissions,
|
221
|
+
@overwrite,
|
222
|
+
File.basename(@previous_deliverables.disk) => @previous_deliverables.disk)
|
223
|
+
ensure
|
224
|
+
uploader.disconnect if uploader.connected?
|
225
|
+
end
|
226
|
+
|
227
|
+
# Preferentially choose user settings
|
228
|
+
def generate_xml(guest)
|
229
|
+
build_xml(:domain_type => (@domain_type || guest.domain_type),
|
230
|
+
:os_type => (@virt_type || guest.os_type),
|
231
|
+
:bus => (@bus || guest.bus))
|
232
|
+
end
|
233
|
+
|
234
|
+
# Build the XML domain definition. If the user provides a script, it will be called after
|
235
|
+
# the basic definition has been constructed with the XML as the sole parameter. The output
|
236
|
+
# from stdout of the script will be used as the new domain definition.
|
237
|
+
def build_xml(opts = {})
|
238
|
+
opts = {:bus => @bus, :os_type => :hvm}.merge!(opts)
|
239
|
+
|
240
|
+
builder = Builder::XmlMarkup.new(:indent => 2)
|
241
|
+
|
242
|
+
xml = builder.domain(:type => opts[:domain_type].to_s) do |domain|
|
243
|
+
domain.name(@appliance_name)
|
244
|
+
domain.description(@appliance_config.summary)
|
245
|
+
domain.memory(@appliance_config.hardware.memory * 1024) #KB
|
246
|
+
domain.vcpu(@appliance_config.hardware.cpus)
|
247
|
+
domain.os do |os|
|
248
|
+
os.type(opts[:os_type].to_s, :arch => @appliance_config.hardware.arch)
|
249
|
+
os.boot(:dev => 'hd')
|
250
|
+
end
|
251
|
+
domain.devices do |devices|
|
252
|
+
devices.disk(:type => 'file', :device => 'disk') do |disk|
|
253
|
+
disk.source(:file => "#{@libvirt_image_uri}/#{File.basename(@previous_deliverables.disk)}")
|
254
|
+
disk.target(:dev => 'hda', :bus => opts[:bus].to_s)
|
255
|
+
end
|
256
|
+
devices.interface(:type => 'network') do |interface|
|
257
|
+
interface.source(:network => @network)
|
258
|
+
interface.mac(:address => @mac) if @mac
|
259
|
+
end
|
260
|
+
devices.console(:type => 'pty') unless @noautoconsole
|
261
|
+
devices.graphics(:type => 'vnc', :port => -1) unless @novnc
|
262
|
+
end
|
263
|
+
domain.features do |features|
|
264
|
+
features.pae if @appliance_config.os.pae
|
265
|
+
end
|
266
|
+
end
|
267
|
+
@log.debug xml
|
268
|
+
|
269
|
+
# Let the user modify the XML specification to their requirements
|
270
|
+
if @script
|
271
|
+
@log.info "Attempting to run user provided script for modifying libVirt XML..."
|
272
|
+
xml = IO::popen("#{@script} --domain '#{xml}'").read
|
273
|
+
@log.debug "Response was: #{xml}"
|
274
|
+
end
|
275
|
+
xml
|
276
|
+
end
|
277
|
+
|
278
|
+
private
|
279
|
+
|
280
|
+
# Look up a domain by name
|
281
|
+
def get_existing_domain(conn, name)
|
282
|
+
return conn.lookup_domain_by_name(name)
|
283
|
+
rescue Libvirt::Error => e
|
284
|
+
return nil if e.libvirt_code == 42 # If domain not defined
|
285
|
+
raise # Otherwise reraise
|
286
|
+
end
|
287
|
+
|
288
|
+
# Undefine a domain. The domain will be destroyed first if required.
|
289
|
+
def undefine_domain(dom)
|
290
|
+
case dom.info.state
|
291
|
+
when Libvirt::Domain::RUNNING, Libvirt::Domain::PAUSED, Libvirt::Domain::BLOCKED
|
292
|
+
dom.destroy
|
293
|
+
end
|
294
|
+
dom.undefine
|
295
|
+
end
|
296
|
+
|
297
|
+
# Libvirt library in older version of Fedora provides no way of getting the
|
298
|
+
# libvirt_code for errors, this patches it in.
|
299
|
+
def libvirt_code_patch
|
300
|
+
return if Libvirt::Error.respond_to?(:libvirt_code, false)
|
301
|
+
Libvirt::Error.module_eval do
|
302
|
+
def libvirt_code; @libvirt_code end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Write domain XML to file
|
307
|
+
def write_xml(xml)
|
308
|
+
fname = "#{@appliance_name}.xml"
|
309
|
+
File.open("#{@dir.tmp}/#{fname}", 'w'){|f| f.write(xml)}
|
310
|
+
register_deliverable(:xml => fname)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|