boxgrinder-build 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/CHANGELOG +21 -0
  2. data/Manifest +92 -0
  3. data/Rakefile +9 -0
  4. data/boxgrinder-build.gemspec +4 -4
  5. data/integ/appliances/_hardware_cpus.appl +13 -0
  6. data/integ/appliances/_hardware_memory.appl +13 -0
  7. data/integ/appliances/_hardware_partitions_home.appl +15 -0
  8. data/integ/appliances/_hardware_partitions_root.appl +15 -0
  9. data/integ/appliances/_os_password.appl +15 -0
  10. data/integ/appliances/_packages_groups_base.appl +13 -0
  11. data/integ/appliances/_packages_groups_core.appl +13 -0
  12. data/integ/appliances/_packages_squid.appl +13 -0
  13. data/integ/appliances/_packages_utils.appl +7 -0
  14. data/integ/appliances/_repos_boxgrinder_permanent_noarch.appl +14 -0
  15. data/integ/appliances/_repos_testlocal_ephemeral_noarch.appl +17 -0
  16. data/integ/appliances/_test_base.appl +22 -0
  17. data/integ/appliances/gnome-fedora.appl +19 -0
  18. data/integ/appliances/jeos-centos.appl +4 -0
  19. data/integ/appliances/jeos-fedora.appl +5 -0
  20. data/integ/appliances/modular.appl +16 -0
  21. data/integ/packages/ephemeral-repo-test-0.1-1.noarch.rpm +0 -0
  22. data/integ/packages/local-repo-test.spec +20 -0
  23. data/integ/spec/jeos-spec.rb +69 -0
  24. data/integ/spec/modular-spec.rb +71 -0
  25. data/lib/boxgrinder-build/appliance.rb +48 -48
  26. data/lib/boxgrinder-build/helpers/guestfs-helper.rb +11 -8
  27. data/lib/boxgrinder-build/helpers/plugin-helper.rb +1 -0
  28. data/lib/boxgrinder-build/helpers/qemu.wrapper +1 -1
  29. data/lib/boxgrinder-build/plugins/base-plugin.rb +55 -14
  30. data/lib/boxgrinder-build/plugins/delivery/ebs/ebs-plugin.rb +270 -54
  31. data/lib/boxgrinder-build/plugins/delivery/elastichosts/elastichosts-plugin.rb +3 -4
  32. data/lib/boxgrinder-build/plugins/delivery/local/local-plugin.rb +20 -16
  33. data/lib/boxgrinder-build/plugins/delivery/s3/s3-plugin.rb +57 -20
  34. data/lib/boxgrinder-build/plugins/delivery/sftp/sftp-plugin.rb +6 -4
  35. data/lib/boxgrinder-build/plugins/os/fedora/fedora-plugin.rb +2 -1
  36. data/lib/boxgrinder-build/plugins/os/rpm-based/kickstart.rb +2 -32
  37. data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-based-os-plugin.rb +29 -4
  38. data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-dependency-validator.rb +15 -46
  39. data/lib/boxgrinder-build/plugins/os/sl/sl-plugin.rb +56 -0
  40. data/lib/boxgrinder-build/plugins/platform/ec2/ec2-plugin.rb +1 -0
  41. data/lib/boxgrinder-build/plugins/platform/ec2/src/rc_local +15 -8
  42. data/lib/boxgrinder-build/plugins/platform/vmware/vmware-plugin.rb +8 -6
  43. data/rubygem-boxgrinder-build.spec +23 -3
  44. data/spec/appliance-spec.rb +114 -73
  45. data/spec/helpers/guestfs-helper-spec.rb +12 -3
  46. data/spec/plugins/base-plugin-spec.rb +24 -18
  47. data/spec/plugins/delivery/ebs/ebs-plugin-spec.rb +206 -67
  48. data/spec/plugins/delivery/elastichosts/elastichosts-plugin-spec.rb +228 -225
  49. data/spec/plugins/delivery/local/local-plugin-spec.rb +13 -34
  50. data/spec/plugins/delivery/s3/s3-plugin-spec.rb +107 -50
  51. data/spec/plugins/os/centos/centos-plugin-spec.rb +1 -1
  52. data/spec/plugins/os/fedora/fedora-plugin-spec.rb +14 -8
  53. data/spec/plugins/os/rhel/rhel-plugin-spec.rb +1 -1
  54. data/spec/plugins/os/rpm-based/kickstart-spec.rb +0 -44
  55. data/spec/plugins/os/rpm-based/rpm-based-os-plugin-spec.rb +31 -2
  56. data/spec/plugins/os/rpm-based/rpm-dependency-validator-spec.rb +20 -7
  57. data/spec/plugins/os/sl/sl-plugin-spec.rb +44 -0
  58. data/spec/plugins/platform/ec2/ec2-plugin-spec.rb +1 -1
  59. data/spec/plugins/platform/virtualbox/virtualbox-plugin-spec.rb +6 -6
  60. data/spec/plugins/platform/vmware/vmware-plugin-spec.rb +14 -13
  61. metadata +27 -5
  62. data/spec/Rakefile +0 -32
@@ -0,0 +1,71 @@
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 'rubygems'
20
+ require 'boxgrinder-build/appliance'
21
+ require 'boxgrinder-core/models/config'
22
+ require 'boxgrinder-core/helpers/log-helper'
23
+ require 'boxgrinder-build/helpers/guestfs-helper'
24
+ require 'fileutils'
25
+
26
+ module BoxGrinder
27
+ describe 'BoxGrinder Build' do
28
+ before(:all) do
29
+ # Cleaning up before build
30
+ FileUtils.rm_rf('build/')
31
+
32
+ # Prepare local repository
33
+ FileUtils.mkdir_p "/tmp/boxgrinder-repo/"
34
+ FileUtils.cp "#{File.dirname(__FILE__)}/../packages/ephemeral-repo-test-0.1-1.noarch.rpm", "/tmp/boxgrinder-repo/"
35
+ system "createrepo /tmp/boxgrinder-repo/"
36
+ end
37
+
38
+ after(:all) do
39
+ # Cleaning up after build
40
+ FileUtils.rm_rf('build/')
41
+ end
42
+
43
+ before(:each) do
44
+ # Deliver the packaged appliance to CloudFront
45
+ @config = Config.new(:delivery => :cloudfront)
46
+ @log = LogHelper.new(:level => :trace, :type => :stdout)
47
+ end
48
+
49
+ after(:each) do
50
+ # Make sure all deliverables really exists
51
+ @appliance.plugin_chain.last[:plugin].deliverables.each_value do |file|
52
+ File.exists?(file).should == true
53
+ end
54
+ end
55
+
56
+ context "modular appliances" do
57
+ it "should build modular appliance based on Fedora and convert it to VirtualBox" do
58
+ @config.merge!(:platform => :virtualbox)
59
+ @appliance = Appliance.new("#{File.dirname(__FILE__)}/../appliances/modular.appl", @config, :log => @log).create
60
+
61
+ GuestFSHelper.new([@appliance.plugin_chain[1][:plugin].deliverables[:disk]], @appliance.appliance_config, @config, :log => @log ).customize do |guestfs, guestfs_helper|
62
+ guestfs.exists('/fedora-boxgrinder-test').should == 1
63
+ guestfs.exists('/common-test-base-boxgrinder-test').should == 1
64
+ guestfs.exists('/hardware-cpus-boxgrinder-test').should == 1
65
+ guestfs.exists('/repos-boxgrinder-noarch-ephemeral-boxgrinder-test').should == 1
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
@@ -28,12 +28,16 @@ require 'boxgrinder-build/managers/plugin-manager'
28
28
 
29
29
  module BoxGrinder
30
30
  class Appliance
31
+ attr_reader :plugin_chain
32
+ attr_reader :appliance_config
33
+
31
34
  def initialize(appliance_definition, config = Config.new, options = {})
32
35
  @appliance_definition = appliance_definition
33
36
  @config = config
34
37
  @log = options[:log] || LogHelper.new(:level => @config.log_level)
35
38
  end
36
39
 
40
+ # TODO: this is not very clean...
37
41
  def read_definition
38
42
  # first try to read as appliance definition file
39
43
  appliance_helper = ApplianceDefinitionHelper.new(:log => @log)
@@ -59,14 +63,37 @@ module BoxGrinder
59
63
  end
60
64
 
61
65
  def validate_definition
62
- raise "No operating system plugins installed. Install one or more operating system plugin. See http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#Operating_system_plugins for more info." if PluginManager.instance.plugins[:os].empty?
63
-
64
66
  os_plugin = PluginManager.instance.plugins[:os][@appliance_config.os.name.to_sym]
65
67
 
66
68
  raise "Not supported operating system selected: #{@appliance_config.os.name}. Make sure you have installed right operating system plugin, see http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#Operating_system_plugins. Supported OSes are: #{PluginManager.instance.plugins[:os].keys.join(", ")}" if os_plugin.nil?
67
69
  raise "Not supported operating system version selected: #{@appliance_config.os.version}. Supported versions are: #{os_plugin[:versions].join(", ")}" unless @appliance_config.os.version.nil? or os_plugin[:versions].include?(@appliance_config.os.version)
68
70
  end
69
71
 
72
+ # Here we initialize all required plugins and create a plugin chain.
73
+ # Initialization involves also plugin configuration validation for specified plugin type.
74
+ def initialize_plugins
75
+ @plugin_chain = []
76
+
77
+ os_plugin, os_plugin_info = PluginManager.instance.initialize_plugin(:os, @appliance_config.os.name.to_sym)
78
+ os_plugin.init(@config, @appliance_config, os_plugin_info, :log => @log)
79
+
80
+ @plugin_chain << {:plugin => os_plugin, :param => @appliance_definition}
81
+
82
+ if platform_selected?
83
+ platform_plugin, platform_plugin_info = PluginManager.instance.initialize_plugin(:platform, @config.platform)
84
+ platform_plugin.init(@config, @appliance_config, platform_plugin_info, :log => @log, :previous_plugin => @plugin_chain.last[:plugin])
85
+
86
+ @plugin_chain << {:plugin => platform_plugin}
87
+ end
88
+
89
+ if delivery_selected?
90
+ delivery_plugin, delivery_plugin_info = PluginManager.instance.initialize_plugin(:delivery, @config.delivery)
91
+ delivery_plugin.init(@config, @appliance_config, delivery_plugin_info, :log => @log, :previous_plugin => @plugin_chain.last[:plugin], :type => @config.delivery)
92
+
93
+ @plugin_chain << {:plugin => delivery_plugin}
94
+ end
95
+ end
96
+
70
97
  def remove_old_builds
71
98
  @log.info "Removing previous builds for #{@appliance_config.name} appliance..."
72
99
  FileUtils.rm_rf(@appliance_config.path.build)
@@ -76,72 +103,45 @@ module BoxGrinder
76
103
  def execute_plugin_chain
77
104
  @log.info "Building '#{@appliance_config.name}' appliance for #{@appliance_config.hardware.arch} architecture."
78
105
 
79
- execute_delivery_plugin(execute_platform_plugin(execute_os_plugin))
106
+ @plugin_chain.each { |p| execute_plugin(p[:plugin], p[:param]) }
80
107
  end
81
108
 
82
109
  def create
83
- @log.debug "Launching new BoxGrinder build..."
110
+ @log.debug "Launching new build..."
84
111
  @log.trace "Used configuration: #{@config.to_yaml.gsub(/(\S*(key|account|cert|username|host|password)\S*).*:(.*)/, '\1' + ": <REDACTED>")}"
85
112
 
113
+ # Let's load all plugins first
86
114
  PluginHelper.new(@config, :log => @log).load_plugins
87
115
  read_definition
88
116
  validate_definition
89
- remove_old_builds if @config.force
90
- execute_plugin_chain
91
- end
117
+ initialize_plugins
92
118
 
93
- def execute_os_plugin
94
- raise "No operating system plugins installed. Install one or more operating system plugin. See http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#Operating_system_plugins for more info." if PluginManager.instance.plugins[:os].empty?
95
-
96
- os_plugin, os_plugin_info = PluginManager.instance.initialize_plugin(:os, @appliance_config.os.name.to_sym)
97
- os_plugin.init(@config, @appliance_config, :log => @log, :plugin_info => os_plugin_info)
98
-
99
- if os_plugin.deliverables_exists?
100
- @log.info "Deliverables for #{os_plugin_info[:name]} operating system plugin exists, skipping."
101
- return {:deliverables => os_plugin.deliverables, :plugin_info => os_plugin_info}
102
- end
119
+ remove_old_builds if @config.force
103
120
 
104
- @log.debug "Executing operating system plugin for #{@appliance_config.os.name}..."
105
- os_plugin.run(@appliance_definition)
106
- @log.debug "Operating system plugin executed."
121
+ execute_plugin_chain
107
122
 
108
- {:deliverables => os_plugin.deliverables, :plugin_info => os_plugin_info}
123
+ self
109
124
  end
110
125
 
111
- def execute_platform_plugin(previous_plugin_output)
112
- if @config.platform == :none or @config.platform.to_s.empty? == nil
113
- @log.debug "No platform selected, skipping platform conversion."
114
- return previous_plugin_output
115
- end
116
-
117
- raise "No platform plugins installed. Install one or more platform plugin. See http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#Platform_plugins for more info." if PluginManager.instance.plugins[:platform].empty?
118
-
119
- platform_plugin, platform_plugin_info = PluginManager.instance.initialize_plugin(:platform, @config.platform)
120
- platform_plugin.init(@config, @appliance_config, :log => @log, :plugin_info => platform_plugin_info, :previous_plugin_info => previous_plugin_output[:plugin_info], :previous_deliverables => previous_plugin_output[:deliverables])
121
-
122
- if platform_plugin.deliverables_exists?
123
- @log.info "Deliverables for #{platform_plugin_info[:name]} platform plugin exists, skipping."
124
- return {:deliverables => platform_plugin.deliverables, :plugin_info => platform_plugin_info}
125
- end
126
-
127
- @log.debug "Executing platform plugin for #{@config.platform}..."
128
- platform_plugin.run
129
- @log.debug "Platform plugin executed."
126
+ def platform_selected?
127
+ !(@config.platform == :none or @config.platform.to_s.empty? == nil)
128
+ end
130
129
 
131
- {:deliverables => platform_plugin.deliverables, :plugin_info => platform_plugin_info}
130
+ def delivery_selected?
131
+ !(@config.delivery == :none or @config.delivery.to_s.empty? == nil)
132
132
  end
133
133
 
134
- def execute_delivery_plugin(previous_plugin_output)
135
- if @config.delivery == :none or @config.delivery.to_s.empty? == nil
136
- @log.debug "No delivery method selected, skipping delivering."
134
+ def execute_plugin(plugin, param = nil)
135
+ if plugin.deliverables_exists?
136
+ @log.info "Deliverables for #{plugin.plugin_info[:name]} #{plugin.plugin_info[:type]} plugin exists, skipping."
137
137
  return
138
138
  end
139
139
 
140
- raise "No delivery plugins installed. Install one or more delivery plugin. See http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#Delivery_plugins for more info" if PluginManager.instance.plugins[:delivery].empty?
140
+ @log.debug "Executing #{plugin.plugin_info[:type]} plugin for #{@appliance_config.os.name}..."
141
141
 
142
- delivery_plugin, delivery_plugin_info = PluginManager.instance.initialize_plugin(:delivery, @config.delivery)
143
- delivery_plugin.init(@config, @appliance_config, :log => @log, :plugin_info => delivery_plugin_info, :previous_plugin_info => previous_plugin_output[:plugin_info], :previous_deliverables => previous_plugin_output[:deliverables])
144
- delivery_plugin.run(@config.delivery)
142
+ param.nil? ? plugin.run : plugin.run(param)
143
+
144
+ @log.debug "Operating system plugin executed."
145
145
  end
146
146
  end
147
147
  end
@@ -20,7 +20,9 @@ require 'boxgrinder-build/helpers/augeas-helper'
20
20
  require 'boxgrinder-core/helpers/log-helper'
21
21
  require 'guestfs'
22
22
  require 'rbconfig'
23
- require 'resolv'
23
+ require 'net/http'
24
+ require 'uri'
25
+ require 'timeout'
24
26
 
25
27
  module BoxGrinder
26
28
  class GuestFSHelper
@@ -36,10 +38,11 @@ module BoxGrinder
36
38
  def hw_virtualization_available?
37
39
  @log.trace "Checking if HW virtualization is available..."
38
40
 
41
+ ec2 = false
42
+
39
43
  begin
40
- ec2 = Resolv.getname("169.254.169.254").include?(".ec2.internal")
41
- rescue Resolv::ResolvError
42
- ec2 = false
44
+ Timeout::timeout(2) { ec2 = Net::HTTP.get_response(URI.parse('http://169.254.169.254/latest/meta-data/ami-id')).code.eql?("200") }
45
+ rescue Exception
43
46
  end
44
47
 
45
48
  if `egrep '^flags.*(vmx|svm)' /proc/cpuinfo | wc -l`.chomp.strip.to_i > 0 and !ec2
@@ -54,13 +57,13 @@ module BoxGrinder
54
57
 
55
58
  # https://issues.jboss.org/browse/BGBUILD-83
56
59
  def log_callback
57
- log = Proc.new do |event, event_handle, buf, array|
60
+ default_callback = Proc.new do |event, event_handle, buf, array|
58
61
  buf.chomp!
59
62
 
60
63
  if event == 64
61
- @log.trace "GFS: #{buf}"
62
- else
63
64
  @log.debug "GFS: #{buf}"
65
+ else
66
+ @log.trace "GFS: #{buf}" unless buf.start_with?('recv_from_daemon', 'send_to_daemon')
64
67
  end
65
68
  end
66
69
 
@@ -69,7 +72,7 @@ module BoxGrinder
69
72
  # Guestfs::EVENT_TRACE => 64
70
73
 
71
74
  # Referencing int instead of constants make it easier to test
72
- @guestfs.set_event_callback(log, 16 | 32 | 64)
75
+ @guestfs.set_event_callback(default_callback, 16 | 32 | 64)
73
76
 
74
77
  yield if block_given?
75
78
  end
@@ -33,6 +33,7 @@ require 'boxgrinder-build/plugins/platform/virtualbox/virtualbox-plugin'
33
33
  require 'boxgrinder-build/plugins/os/centos/centos-plugin'
34
34
  require 'boxgrinder-build/plugins/os/rhel/rhel-plugin'
35
35
  require 'boxgrinder-build/plugins/os/fedora/fedora-plugin'
36
+ require 'boxgrinder-build/plugins/os/sl/sl-plugin'
36
37
 
37
38
  module BoxGrinder
38
39
  class PluginHelper
@@ -6,7 +6,7 @@ else
6
6
  args=( "$@" )
7
7
  fi
8
8
 
9
- if [ "`uname -r | grep x86_64 | wc -l`" -eq "1" ]; then
9
+ if [ "`uname -a | grep x86_64 | wc -l`" -eq "1" ]; then
10
10
  bin=/usr/bin/qemu-system-x86_64
11
11
  else
12
12
  bin=/usr/bin/qemu
@@ -29,6 +29,9 @@ require 'logger'
29
29
 
30
30
  module BoxGrinder
31
31
  class BasePlugin
32
+ attr_reader :plugin_info
33
+ attr_reader :deliverables
34
+
32
35
  def initialize
33
36
  @plugin_config = {}
34
37
 
@@ -38,32 +41,74 @@ module BoxGrinder
38
41
  @dir = OpenCascade.new
39
42
  end
40
43
 
41
- def init(config, appliance_config, options = {})
44
+ def init(config, appliance_config, info, options = {})
42
45
  @config = config
43
46
  @appliance_config = appliance_config
44
47
  @options = options
48
+ @plugin_info = info
49
+
50
+ # Optional options :)
51
+ @type = options[:type] || @plugin_info[:name]
52
+ @previous_plugin = options[:previous_plugin]
45
53
  @log = options[:log] || LogHelper.new
46
54
  @exec_helper = options[:exec_helper] || ExecHelper.new(:log => @log)
47
55
  @image_helper = options[:image_helper] || ImageHelper.new(@config, @appliance_config, :log => @log)
48
- @previous_plugin_info = options[:previous_plugin_info]
49
- @previous_deliverables = options[:previous_deliverables] || OpenCascade.new
50
-
51
- @plugin_info = options[:plugin_info]
52
56
 
53
57
  @dir.base = "#{@appliance_config.path.build}/#{@plugin_info[:name]}-plugin"
54
58
  @dir.tmp = "#{@dir.base}/tmp"
55
59
 
60
+ if @previous_plugin
61
+ @previous_deliverables = @previous_plugin.deliverables
62
+ @previous_plugin_info = @previous_plugin.plugin_info
63
+ else
64
+ @previous_deliverables = OpenCascade.new
65
+ end
66
+
67
+ # TODO get rid of that - we don't have plugin configuration files - everything is now in one place.
56
68
  read_plugin_config
57
69
  merge_plugin_config
58
70
 
71
+ # Indicate whether deliverables of select plugin should be moved to final destination or not.
72
+ # TODO Needs some thoughts - if we don't have deliverables that we care about - should they be in @deliverables?
59
73
  @move_deliverables = true
74
+
75
+ # Validate the plugin configuration.
76
+ # Please make the validate method as simple as possible, because it'll be executed also in unit tests.
77
+ validate
78
+
79
+ # The plugin is initialized now. We can do some fancy stuff with it.
60
80
  @initialized = true
61
81
 
82
+ # If there is something defined in the plugin that should be executed after plugin initialization - it should go
83
+ # to after_init method.
62
84
  after_init
63
85
 
64
86
  self
65
87
  end
66
88
 
89
+ # This is a stub that should be overriden by the actual plugin implementation.
90
+ # It can use subtype(:TYPE) calls to validate for a specific type.
91
+ # KISS!
92
+ def validate
93
+ end
94
+
95
+ # Callback - executed after initialization.
96
+ def after_init
97
+ end
98
+
99
+ # Callback - executed after execution.
100
+ def after_execute
101
+ end
102
+
103
+ # Validation helper method.
104
+ #
105
+ # Execute the validation only for selected plugin type.
106
+ # TODO make this prettier somehow? Maybe moving validation to a class?
107
+ def subtype(type)
108
+ return unless @type == type
109
+ yield if block_given?
110
+ end
111
+
67
112
  def register_deliverable(deliverable)
68
113
  raise "You can only register deliverables after the plugin is initialized, please initialize the plugin using init method." if @initialized.nil?
69
114
  raise "Please specify deliverables as Hash, not #{deliverable.class}." unless deliverable.is_a?(Hash)
@@ -120,7 +165,7 @@ module BoxGrinder
120
165
  raise "You can only execute the plugin after the plugin is initialized, please initialize the plugin using init method." if @initialized.nil?
121
166
  end
122
167
 
123
- def run(*args)
168
+ def run(param = nil)
124
169
  unless is_supported_os?
125
170
  @log.error "#{@plugin_info[:full_name]} plugin supports following operating systems: #{supported_oses}. Your appliance contains #{@appliance_config.os.name} #{@appliance_config.os.version} operating system which is not supported by this plugin, sorry."
126
171
  return
@@ -129,7 +174,7 @@ module BoxGrinder
129
174
  FileUtils.rm_rf @dir.tmp
130
175
  FileUtils.mkdir_p @dir.tmp
131
176
 
132
- execute(*args)
177
+ param.nil? ? execute : execute(param)
133
178
 
134
179
  # TODO execute post commands for platform plugins here?
135
180
 
@@ -141,18 +186,14 @@ module BoxGrinder
141
186
  FileUtils.rm_rf @dir.tmp
142
187
  end
143
188
 
144
- def after_init
145
- end
146
-
147
- def after_execute
148
- end
149
-
150
189
  def deliverables_exists?
151
190
  raise "You can only check deliverables after the plugin is initialized, please initialize the plugin using init method." if @initialized.nil?
152
191
 
192
+ return false if deliverables.empty?
193
+
153
194
  exists = true
154
195
 
155
- @target_deliverables.each_value do |file|
196
+ deliverables.each_value do |file|
156
197
  unless File.exists?(file)
157
198
  exists = false
158
199
  break
@@ -20,58 +20,105 @@ require 'rubygems'
20
20
  require 'boxgrinder-build/plugins/base-plugin'
21
21
  require 'AWS'
22
22
  require 'open-uri'
23
+ require 'timeout'
24
+ require 'pp'
23
25
 
24
26
  module BoxGrinder
25
27
  class EBSPlugin < BasePlugin
26
28
  KERNELS = {
27
29
  'eu-west-1' => {
28
- 'i386' => {:aki => 'aki-4deec439'},
29
- 'x86_64' => {:aki => 'aki-4feec43b'}
30
+ :endpoint => 'ec2.eu-west-1.amazonaws.com',
31
+ :location => 'EU',
32
+ :kernel => {
33
+ 'i386' => {:aki => 'aki-4deec439'},
34
+ 'x86_64' => {:aki => 'aki-4feec43b'}
35
+ }
30
36
  },
37
+
31
38
  'ap-southeast-1' => {
32
- 'i386' => {:aki => 'aki-13d5aa41'},
33
- 'x86_64' => {:aki => 'aki-11d5aa43'}
39
+ :endpoint => 'ec2.ap-southeast-1.amazonaws.com',
40
+ :location => 'ap-southeast-1',
41
+ :kernel => {
42
+ 'i386' => {:aki => 'aki-13d5aa41'},
43
+ 'x86_64' => {:aki => 'aki-11d5aa43'}
44
+ }
45
+ },
46
+
47
+ 'ap-northeast-1' => {
48
+ :endpoint => 'ec2.ap-northeast-1.amazonaws.com',
49
+ :location => 'ap-northeast-1',
50
+ :kernel => {
51
+ 'i386' => {:aki => 'aki-d209a2d3'},
52
+ 'x86_64' => {:aki => 'aki-d409a2d5'}
53
+ }
54
+
34
55
  },
56
+
35
57
  'us-west-1' => {
36
- 'i386' => {:aki => 'aki-99a0f1dc'},
37
- 'x86_64' => {:aki => 'aki-9ba0f1de'}
58
+ :endpoint => 'ec2.us-west-1.amazonaws.com',
59
+ :location => 'us-west-1',
60
+ :kernel => {
61
+ 'i386' => {:aki => 'aki-99a0f1dc'},
62
+ 'x86_64' => {:aki => 'aki-9ba0f1de'}
63
+ }
38
64
  },
65
+
39
66
  'us-east-1' => {
40
- 'i386' => {:aki => 'aki-407d9529'},
41
- 'x86_64' => {:aki => 'aki-427d952b'}
67
+ :endpoint => 'ec2.amazonaws.com',
68
+ :location => '',
69
+ :kernel => {
70
+ 'i386' => {:aki => 'aki-407d9529'},
71
+ 'x86_64' => {:aki => 'aki-427d952b'}
72
+ }
42
73
  }
43
74
  }
44
75
 
45
- def after_init
46
- if valid_platform?
47
- @current_avaibility_zone = open('http://169.254.169.254/latest/meta-data/placement/availability-zone').string
48
- @region = @current_avaibility_zone.scan(/((\w+)-(\w+)-(\d+))/).flatten.first
49
- end
76
+ ROOT_DEVICE_NAME = '/dev/sda1'
77
+ POLL_FREQ = 1 #second
78
+ TIMEOUT = 1000 #seconds
79
+ EC2_HOSTNAME_LOOKUP_TIMEOUT = 10
50
80
 
51
- set_default_config_value('availability_zone', @current_avaibility_zone)
81
+ def validate
82
+ raise PluginValidationError, "You are trying to run this plugin on invalid platform. You can run the EBS delivery plugin only on EC2." unless valid_platform?
83
+
84
+ @current_availability_zone = get_ec2_availability_zone; @log.trace @current_availability_zone
85
+
86
+ set_default_config_value('availability_zone', @current_availability_zone)
52
87
  set_default_config_value('delete_on_termination', true)
88
+ set_default_config_value('overwrite', false)
89
+ set_default_config_value('snapshot', false)
90
+ set_default_config_value('preserve_snapshots', false)
91
+ validate_plugin_config(['access_key', 'secret_access_key', 'account_number'], 'http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#EBS_Delivery_Plugin')
53
92
 
54
- register_supported_os('fedora', ['13', '14', '15'])
55
- register_supported_os('rhel', ['6'])
93
+ raise PluginValidationError, "You can only convert to EBS type AMI appliances converted to EC2 format. Use '-p ec2' switch. For more info about EC2 plugin see http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#EC2_Platform_Plugin." unless @previous_plugin_info[:name] == :ec2
94
+ raise PluginValidationError, "You selected #{@plugin_config['availability_zone']} availability zone, but your instance is running in #{@current_availability_zone} zone. Please change availability zone in plugin configuration file to #{@current_availability_zone} (see http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#EBS_Delivery_Plugin) or use another instance in #{@plugin_config['availability_zone']} zone to create your EBS AMI." if @plugin_config['availability_zone'] != @current_availability_zone
56
95
  end
57
96
 
58
- def execute(type = :ebs)
59
- validate_plugin_config(['access_key', 'secret_access_key', 'account_number'], 'http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#EBS_Delivery_Plugin')
97
+ def after_init
98
+ @region = availability_zone_to_region(@current_availability_zone)
60
99
 
61
- raise "You try to run this plugin on invalid platform. You can run EBS delivery plugin only on EC2." unless valid_platform?
62
- raise "You can only convert to EBS type AMI appliances converted to EC2 format. Use '-p ec2' switch. For more info about EC2 plugin see http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#EC2_Platform_Plugin." unless @previous_plugin_info[:name] == :ec2
63
- raise "You selected #{@plugin_config['availability_zone']} avaibility zone, but your instance is running in #{@current_avaibility_zone} zone. Please change avaibility zone in plugin configuration file to #{@current_avaibility_zone} (see http://boxgrinder.org/tutorials/boxgrinder-build-plugins/#EBS_Delivery_Plugin) or use another instance in #{@plugin_config['availability_zone']} zone to create your EBS AMI." if @plugin_config['availability_zone'] != @current_avaibility_zone
100
+ register_supported_os('fedora', ['13', '14', '15'])
101
+ register_supported_os('rhel', ['6'])
102
+ register_supported_os('centos', ['5'])
103
+ end
64
104
 
105
+ def execute
65
106
  ebs_appliance_description = "#{@appliance_config.summary} | Appliance version #{@appliance_config.version}.#{@appliance_config.release} | #{@appliance_config.hardware.arch} architecture"
66
107
 
67
- @ec2 = AWS::EC2::Base.new(:access_key_id => @plugin_config['access_key'], :secret_access_key => @plugin_config['secret_access_key'])
108
+ @ec2 = AWS::EC2::Base.new(:access_key_id => @plugin_config['access_key'],
109
+ :secret_access_key => @plugin_config['secret_access_key'],
110
+ :server => KERNELS[@region][:endpoint]
111
+ )
68
112
 
69
113
  @log.debug "Checking if appliance is already registered..."
70
114
 
71
- ami_id = already_registered?(ebs_appliance_name)
115
+ ami_info = ami_info(ebs_appliance_name)
72
116
 
73
- if ami_id
74
- @log.warn "EBS AMI '#{ebs_appliance_name}' is already registered as '#{ami_id}' (region: #{@region})."
117
+ if ami_info and @plugin_config['overwrite']
118
+ @log.info "Overwrite is enabled. Stomping existing assets"
119
+ stomp_ebs(ami_info)
120
+ elsif ami_info
121
+ @log.warn "EBS AMI '#{ebs_appliance_name}' is already registered as '#{ami_info.imageId}' (region: #{@region})."
75
122
  return
76
123
  end
77
124
 
@@ -81,13 +128,15 @@ module BoxGrinder
81
128
 
82
129
  @appliance_config.hardware.partitions.each_value { |partition| size += partition['size'] }
83
130
 
84
- # create_volume with 10GB size
85
- volume_id = @ec2.create_volume(:size => size.to_s, :availability_zone => @plugin_config['availability_zone'])['volumeId']
131
+ # create_volume, ceiling to avoid fractions as per https://issues.jboss.org/browse/BGBUILD-224
132
+ volume_id = @ec2.create_volume(:size => size.ceil.to_s, :availability_zone => @plugin_config['availability_zone'])['volumeId']
133
+
134
+ begin
86
135
 
87
136
  @log.debug "Volume #{volume_id} created."
88
137
  @log.debug "Waiting for EBS volume #{volume_id} to be available..."
89
138
 
90
- # wait fo volume to be created
139
+ # wait for volume to be created
91
140
  wait_for_volume_status('available', volume_id)
92
141
 
93
142
  # get first free device to mount the volume
@@ -110,7 +159,9 @@ module BoxGrinder
110
159
  # wait for volume to be attached
111
160
  wait_for_volume_status('in-use', volume_id)
112
161
 
113
- sleep 10 # let's wait to discover the attached volume by OS
162
+ @log.debug "Waiting for the attached EBS volume to be discovered by the OS"
163
+
164
+ wait_for_volume_attachment(suffix) # add rescue block for timeout when no suffix can be found then re-raise
114
165
 
115
166
  @log.info "Copying data to EBS volume..."
116
167
 
@@ -125,7 +176,7 @@ module BoxGrinder
125
176
 
126
177
  @ec2.detach_volume(:device => "/dev/sd#{suffix}", :volume_id => volume_id, :instance_id => instance_id)
127
178
 
128
- @log.debug "Waiting for EBS volume to be available..."
179
+ @log.debug "Waiting for EBS volume to become available..."
129
180
 
130
181
  wait_for_volume_status('available', volume_id)
131
182
 
@@ -167,15 +218,124 @@ module BoxGrinder
167
218
  :device_name => '/dev/sde',
168
219
  :virtual_name => 'ephemeral3'
169
220
  }],
170
- :root_device_name => '/dev/sda1',
221
+ :root_device_name => ROOT_DEVICE_NAME,
171
222
  :architecture => @appliance_config.hardware.base_arch,
172
- :kernel_id => KERNELS[@region][@appliance_config.hardware.base_arch][:aki],
223
+ :kernel_id => KERNELS[@region][:kernel][@appliance_config.hardware.base_arch][:aki],
173
224
  :name => ebs_appliance_name,
174
225
  :description => ebs_appliance_description)['imageId']
175
226
 
227
+ rescue Timeout::Error
228
+ @log.error "Timed out. Manual intervention may be necessary to complete the task."
229
+ raise
230
+ end
231
+
176
232
  @log.info "EBS AMI '#{ebs_appliance_name}' registered: #{image_id} (region: #{@region})"
177
233
  end
178
234
 
235
+ def get_volume_info(volume_id)
236
+ begin
237
+ @ec2.describe_volumes(:volume_id => volume_id).volumeSet.item.each do |volume|
238
+ return volume if volume.volumeId == volume_id
239
+ end
240
+ rescue AWS::Error, AWS::InvalidVolumeIDNotFound => e# only InvalidVolumeIDNotFound should be returned when no volume found, but is not always doing so at present.
241
+ @log.trace "Error getting volume info: #{e}"
242
+ return nil
243
+ end
244
+ nil
245
+ end
246
+
247
+ def snapshot_info(snapshot_id)
248
+ begin
249
+ @ec2.describe_snapshots(:snapshot_id => snapshot_id).snapshotSet.item.each do |snapshot|
250
+ return snapshot if snapshot.snapshotId == snapshot_id
251
+ end
252
+ rescue AWS::InvalidSnapshotIDNotFound
253
+ return nil
254
+ end
255
+ nil
256
+ end
257
+
258
+ def block_device_from_ami(ami_info, device_name)
259
+ ami_info.blockDeviceMapping.item.each do |device|
260
+ return device if device.deviceName == device_name
261
+ end
262
+ nil
263
+ end
264
+
265
+ def get_instances(ami_id)
266
+ #EC2 Gem has yet to be updated with new filters, once the patches have been pulled then :image_id filter will be picked up
267
+ instances_info = @ec2.describe_instances(:image_id => ami_id).reservationSet
268
+ instances=[]
269
+ instances_info["item"].each do
270
+ |item| item["instancesSet"]["item"].each do |i|
271
+ instances.push i if i.imageId == ami_id #TODO remove check after gem update
272
+ end
273
+ end
274
+ return instances.uniq unless instances.empty?
275
+ nil
276
+ end
277
+
278
+ def stomp_ebs(ami_info)
279
+
280
+ device = block_device_from_ami(ami_info, ROOT_DEVICE_NAME)
281
+
282
+ if device #if there is the anticipated device on the image
283
+ snapshot_info = snapshot_info(device.ebs.snapshotId)
284
+ volume_id = snapshot_info.volumeId
285
+ volume_info = get_volume_info(volume_id)
286
+
287
+ @log.trace "Volume info for #{volume_id} : #{PP::pp(volume_info,"")}"
288
+ @log.info "Finding any existing image with the block store attached"
289
+
290
+ if instances = get_instances(ami_info.imageId)
291
+ raise "There are still instances of #{ami_info.imageId} running, you must stop them: #{instances.collect {|i| i.instanceId}.join(",")}"
292
+ end
293
+
294
+ if volume_info #if the physical volume exists
295
+ unless volume_info.status == 'available'
296
+ begin
297
+ @log.info "Forcibly detaching block store #{volume_info.volumeId}"
298
+ @ec2.detach_volume(:volume_id => volume_info.volumeId, :force => true)
299
+ rescue AWS::IncorrectState
300
+ @log.debug "State of the volume has changed, our data must have been stale. This should not be fatal."
301
+ end
302
+ end
303
+
304
+ @log.debug "Waiting for volume to become detached"
305
+ wait_for_volume_status('available', volume_info.volumeId)
306
+
307
+ begin
308
+ @log.info "Deleting block store"
309
+ @ec2.delete_volume(:volume_id => volume_info.volumeId)
310
+ @log.debug "Waiting for volume deletion to be confirmed"
311
+ wait_for_volume_status('deleted', volume_info.volumeId)
312
+ rescue AWS::InvalidVolumeIDNotFound
313
+ @log.debug "An external entity has probably deleted the volume just before we tried to. This should not be fatal."
314
+ end
315
+ end
316
+
317
+ begin
318
+ @log.debug "Deregistering AMI"
319
+ @ec2.deregister_image(:image_id => ami_info.imageId)
320
+ rescue AWS::InvalidAMIIDUnavailable, AWS::InvalidAMIIDNotFound
321
+ @log.debug "An external entity has already deregistered the AMI just before we tried to. This should not be fatal."
322
+ end
323
+
324
+ if !@plugin_config['preserve_snapshots'] and snapshot_info #if the snapshot exists
325
+ begin
326
+ @log.debug "Deleting snapshot #{snapshot_info.snapshotId}"
327
+ @ec2.delete_snapshot(:snapshot_id => snapshot_info.snapshotId)
328
+ rescue AWS::InvalidSnapshotIDNotFound
329
+ @log.debug "An external entity has probably deleted the snapshot just before we tried to. This should not be fatal."
330
+ end
331
+ end
332
+ else
333
+ @log.error "Expected device #{ROOT_DEVICE_NAME} was not found on the image."
334
+ return false
335
+ end
336
+ true
337
+ end
338
+
179
339
  def ebs_appliance_name
180
340
  base_path = "#{@appliance_config.name}/#{@appliance_config.os.name}/#{@appliance_config.os.version}/#{@appliance_config.version}.#{@appliance_config.release}"
181
341
 
@@ -186,17 +346,26 @@ module BoxGrinder
186
346
  while already_registered?("#{base_path}-SNAPSHOT-#{snapshot}/#{@appliance_config.hardware.arch}")
187
347
  snapshot += 1
188
348
  end
349
+ # Reuse the last key (if there was one)
350
+ snapshot -=1 if snapshot > 1 and @plugin_config['overwrite']
189
351
 
190
352
  "#{base_path}-SNAPSHOT-#{snapshot}/#{@appliance_config.hardware.arch}"
191
353
  end
192
354
 
193
- def already_registered?(name)
194
- images = @ec2.describe_images(:owner_id => @plugin_config['account_number'].to_s.gsub(/-/, ''))
195
-
196
- return false if images.nil? or images['imagesSet'].nil?
355
+ def ami_info(name)
356
+ images = @ec2.describe_images(:owner_id => @plugin_config['account_number'].to_s.gsub(/-/,''))
357
+ return false if images.nil?
358
+ images = images.imagesSet
197
359
 
198
- images['imagesSet']['item'].each { |image| return image['imageId'] if image['name'] == name }
360
+ for image in images.item do
361
+ return image if image.name == name
362
+ end
363
+ false
364
+ end
199
365
 
366
+ def already_registered?(name)
367
+ info = ami_info(name)
368
+ return info.imageId if info
200
369
  false
201
370
  end
202
371
 
@@ -205,47 +374,94 @@ module BoxGrinder
205
374
  guestfs.mv("/etc/fstab.new", "/etc/fstab")
206
375
  end
207
376
 
208
- def wait_for_snapshot_status(status, snapshot_id)
209
- snapshot = @ec2.describe_snapshots(:snapshot_id => snapshot_id)['snapshotSet']['item'].first
377
+ def wait_with_timeout(cycle_seconds, timeout_seconds)
378
+ Timeout::timeout(timeout_seconds) do
379
+ while not yield
380
+ sleep cycle_seconds
381
+ end
382
+ end
383
+ end
210
384
 
211
- unless snapshot['status'] == status
212
- sleep 2
213
- wait_for_snapshot_status(status, snapshot_id)
385
+ def wait_for_volume_attachment(suffix)
386
+ wait_with_timeout(POLL_FREQ, TIMEOUT){ device_for_suffix(suffix) != nil }
387
+ end
388
+
389
+ def wait_for_snapshot_status(status, snapshot_id)
390
+ begin
391
+ progress = -1
392
+ snapshot = nil
393
+ wait_with_timeout(POLL_FREQ, TIMEOUT) do
394
+ snapshot = @ec2.describe_snapshots(:snapshot_id => snapshot_id)['snapshotSet']['item'].first
395
+ current_progress = snapshot.progress.to_i
396
+ unless progress == current_progress
397
+ @log.info "Progress: #{current_progress}%"
398
+ progress = current_progress
399
+ end
400
+ snapshot['status'] == status
401
+ end
402
+ rescue Exception
403
+ snapshot.ownerId='<REDACTED>' #potentially sensitive?
404
+ @log.debug "Polling of snapshot #{snapshot_id} for status '#{status}' failed: " <<
405
+ "#{PP::pp(snapshot)}" unless snapshot.nil?
406
+ raise
214
407
  end
215
408
  end
216
409
 
217
410
  def wait_for_volume_status(status, volume_id)
218
- volume = @ec2.describe_volumes(:volume_id => volume_id)['volumeSet']['item'].first
219
-
220
- unless volume['status'] == status
221
- sleep 2
222
- wait_for_volume_status(status, volume_id)
411
+ begin
412
+ volume=nil
413
+ wait_with_timeout(POLL_FREQ, TIMEOUT) do
414
+ volume = @ec2.describe_volumes(:volume_id => volume_id)['volumeSet']['item'].first
415
+ volume['status'] == status
416
+ end
417
+ rescue Exception
418
+ @log.debug "Polling of volume #{volume_id} for status '#{status}' failed: " <<
419
+ "#{PP::pp(volume)}" unless volume.nil?
420
+ raise
223
421
  end
224
422
  end
225
423
 
226
424
  def device_for_suffix(suffix)
227
425
  return "/dev/sd#{suffix}" if File.exists?("/dev/sd#{suffix}")
228
426
  return "/dev/xvd#{suffix}" if File.exists?("/dev/xvd#{suffix}")
229
-
230
- raise "Device for suffix '#{suffix}' not found!"
427
+ nil
428
+ #raise "Device for suffix '#{suffix}' not found!"
231
429
  end
232
430
 
233
431
  def free_device_suffix
234
432
  ("f".."p").each do |suffix|
235
433
  return suffix unless File.exists?("/dev/sd#{suffix}") or File.exists?("/dev/xvd#{suffix}")
236
434
  end
237
-
238
435
  raise "Found too many attached devices. Cannot attach EBS volume."
239
436
  end
240
437
 
241
438
  def valid_platform?
242
439
  begin
243
- return Resolv.getname("169.254.169.254").include?(".ec2.internal")
244
- rescue Resolv::ResolvError
245
- false
440
+ region = availability_zone_to_region(get_ec2_availability_zone)
441
+ return true if KERNELS.has_key? region
442
+ @log.warn "You may be using an ec2 region that BoxGrinder Build is not aware of: #{region}, BoxGrinder Build knows of: #{KERNELS.join(", ")}"
443
+ rescue Net::HTTPServerException => e
444
+ @log.warn "An error was returned when attempting to retrieve the ec2 hostname: #{e.to_s}"
445
+ rescue Timeout::Error => t
446
+ @log.warn "A timeout occurred while attempting to retrieve the ec2 hostname: #{t.to_s}"
447
+ end
448
+ false
449
+ end
450
+
451
+ def get_ec2_availability_zone
452
+ timeout(EC2_HOSTNAME_LOOKUP_TIMEOUT) do
453
+ req = Net::HTTP::Get.new('/latest/meta-data/placement/availability-zone/')
454
+ res = Net::HTTP.start('169.254.169.254', 80) {|http| http.request(req)}
455
+ return res.body if Net::HTTPSuccess
456
+ res.error!
246
457
  end
247
458
  end
459
+
460
+ def availability_zone_to_region(availability_zone)
461
+ availability_zone.scan(/((\w+)-(\w+)-(\d+))/).flatten.first
462
+ end
463
+
248
464
  end
249
465
  end
250
466
 
251
- plugin :class => BoxGrinder::EBSPlugin, :type => :delivery, :name => :ebs, :full_name => "Elastic Block Storage"
467
+ plugin :class => BoxGrinder::EBSPlugin, :type => :delivery, :name => :ebs, :full_name => "Elastic Block Storage"