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.
Files changed (52) hide show
  1. data/CHANGELOG +15 -0
  2. data/Manifest +7 -22
  3. data/Rakefile +3 -1
  4. data/bin/boxgrinder-build +1 -1
  5. data/boxgrinder-build.gemspec +17 -13
  6. data/integ/appliances/jeos-centos6.appl +4 -0
  7. data/lib/boxgrinder-build/appliance.rb +61 -23
  8. data/lib/boxgrinder-build/helpers/ec2-helper.rb +18 -0
  9. data/lib/boxgrinder-build/helpers/linux-helper.rb +41 -2
  10. data/lib/boxgrinder-build/helpers/plugin-helper.rb +3 -0
  11. data/lib/boxgrinder-build/helpers/s3-helper.rb +18 -0
  12. data/lib/boxgrinder-build/helpers/sftp-helper.rb +124 -0
  13. data/lib/boxgrinder-build/managers/plugin-manager.rb +5 -3
  14. data/lib/boxgrinder-build/plugins/base-plugin.rb +2 -22
  15. data/lib/boxgrinder-build/plugins/delivery/ebs/ebs-plugin.rb +26 -15
  16. data/lib/boxgrinder-build/plugins/delivery/elastichosts/elastichosts-plugin.rb +2 -1
  17. data/lib/boxgrinder-build/plugins/delivery/libvirt/libvirt-capabilities.rb +164 -0
  18. data/lib/boxgrinder-build/plugins/delivery/libvirt/libvirt-plugin.rb +313 -0
  19. data/lib/boxgrinder-build/plugins/delivery/local/local-plugin.rb +2 -1
  20. data/lib/boxgrinder-build/plugins/delivery/openstack/openstack-plugin.rb +133 -0
  21. data/lib/boxgrinder-build/plugins/delivery/s3/s3-plugin.rb +15 -2
  22. data/lib/boxgrinder-build/plugins/delivery/sftp/sftp-plugin.rb +20 -106
  23. data/lib/boxgrinder-build/plugins/os/centos/centos-plugin.rb +3 -3
  24. data/lib/boxgrinder-build/plugins/os/fedora/fedora-plugin.rb +2 -1
  25. data/lib/boxgrinder-build/plugins/os/rhel/rhel-plugin.rb +2 -1
  26. data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-based-os-plugin.rb +6 -64
  27. data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-dependency-validator.rb +2 -1
  28. data/lib/boxgrinder-build/plugins/os/sl/sl-plugin.rb +1 -2
  29. data/lib/boxgrinder-build/plugins/platform/ec2/ec2-plugin.rb +15 -1
  30. data/lib/boxgrinder-build/plugins/platform/virtualbox/virtualbox-plugin.rb +2 -1
  31. data/lib/boxgrinder-build/plugins/platform/virtualpc/virtualpc-plugin.rb +58 -0
  32. data/lib/boxgrinder-build/plugins/platform/vmware/vmware-plugin.rb +2 -1
  33. data/rubygem-boxgrinder-build.spec +25 -1
  34. data/spec/appliance-spec.rb +1 -58
  35. data/spec/helpers/linux-helper-spec.rb +70 -0
  36. data/spec/managers/plugin-manager-spec.rb +4 -13
  37. data/spec/plugins/delivery/ebs/ebs-plugin-spec.rb +6 -14
  38. data/spec/plugins/delivery/elastichosts/elastichosts-plugin-spec.rb +5 -6
  39. data/spec/plugins/delivery/libvirt/libvirt-plugin-spec.rb +300 -0
  40. data/spec/plugins/delivery/libvirt/libvirt_modified.xml +25 -0
  41. data/spec/plugins/delivery/libvirt/libvirt_modify.sh +18 -0
  42. data/spec/plugins/delivery/libvirt/libvirt_test.xml +24 -0
  43. data/spec/plugins/delivery/local/local-plugin-spec.rb +3 -6
  44. data/spec/plugins/delivery/openstack/openstack-plugin-spec.rb +103 -0
  45. data/spec/plugins/delivery/s3/s3-plugin-spec.rb +16 -5
  46. data/spec/plugins/os/rpm-based/rpm-based-os-plugin-spec.rb +2 -80
  47. data/spec/plugins/platform/ec2/ec2-plugin-spec.rb +15 -1
  48. data/spec/plugins/platform/virtualbox/virtualbox-plugin-spec.rb +6 -6
  49. data/spec/plugins/platform/virtualpc/virtualpc-plugin-spec.rb +90 -0
  50. data/spec/plugins/platform/vmware/vmware-plugin-spec.rb +5 -9
  51. data/spec/rspec-plugin-helper.rb +47 -0
  52. 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(args)
24
- PluginManager.instance.register_plugin(args)
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
- image = @ec2.images.create(
149
- :name => ebs_appliance_name,
150
- :root_device_name => ROOT_DEVICE_NAME,
151
- :block_device_mappings => { ROOT_DEVICE_NAME => {
152
- :snapshot => snapshot,
153
- :delete_on_termination => @plugin_config['delete_on_termination']
154
- },
155
- '/dev/sdb' => 'ephemeral0',
156
- '/dev/sdc' => 'ephemeral1',
157
- '/dev/sdd' => 'ephemeral2',
158
- '/dev/sde' => 'ephemeral3'},
159
- :architecture => @appliance_config.hardware.base_arch,
160
- :kernel_id => @ec2_endpoints[@current_region][:kernel][@appliance_config.hardware.base_arch.intern][:aki],
161
- :description => ebs_appliance_description)
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