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.
- data/CHANGELOG +21 -0
- data/Manifest +92 -0
- data/Rakefile +9 -0
- data/boxgrinder-build.gemspec +4 -4
- data/integ/appliances/_hardware_cpus.appl +13 -0
- data/integ/appliances/_hardware_memory.appl +13 -0
- data/integ/appliances/_hardware_partitions_home.appl +15 -0
- data/integ/appliances/_hardware_partitions_root.appl +15 -0
- data/integ/appliances/_os_password.appl +15 -0
- data/integ/appliances/_packages_groups_base.appl +13 -0
- data/integ/appliances/_packages_groups_core.appl +13 -0
- data/integ/appliances/_packages_squid.appl +13 -0
- data/integ/appliances/_packages_utils.appl +7 -0
- data/integ/appliances/_repos_boxgrinder_permanent_noarch.appl +14 -0
- data/integ/appliances/_repos_testlocal_ephemeral_noarch.appl +17 -0
- data/integ/appliances/_test_base.appl +22 -0
- data/integ/appliances/gnome-fedora.appl +19 -0
- data/integ/appliances/jeos-centos.appl +4 -0
- data/integ/appliances/jeos-fedora.appl +5 -0
- data/integ/appliances/modular.appl +16 -0
- data/integ/packages/ephemeral-repo-test-0.1-1.noarch.rpm +0 -0
- data/integ/packages/local-repo-test.spec +20 -0
- data/integ/spec/jeos-spec.rb +69 -0
- data/integ/spec/modular-spec.rb +71 -0
- data/lib/boxgrinder-build/appliance.rb +48 -48
- data/lib/boxgrinder-build/helpers/guestfs-helper.rb +11 -8
- data/lib/boxgrinder-build/helpers/plugin-helper.rb +1 -0
- data/lib/boxgrinder-build/helpers/qemu.wrapper +1 -1
- data/lib/boxgrinder-build/plugins/base-plugin.rb +55 -14
- data/lib/boxgrinder-build/plugins/delivery/ebs/ebs-plugin.rb +270 -54
- data/lib/boxgrinder-build/plugins/delivery/elastichosts/elastichosts-plugin.rb +3 -4
- data/lib/boxgrinder-build/plugins/delivery/local/local-plugin.rb +20 -16
- data/lib/boxgrinder-build/plugins/delivery/s3/s3-plugin.rb +57 -20
- data/lib/boxgrinder-build/plugins/delivery/sftp/sftp-plugin.rb +6 -4
- data/lib/boxgrinder-build/plugins/os/fedora/fedora-plugin.rb +2 -1
- data/lib/boxgrinder-build/plugins/os/rpm-based/kickstart.rb +2 -32
- data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-based-os-plugin.rb +29 -4
- data/lib/boxgrinder-build/plugins/os/rpm-based/rpm-dependency-validator.rb +15 -46
- data/lib/boxgrinder-build/plugins/os/sl/sl-plugin.rb +56 -0
- data/lib/boxgrinder-build/plugins/platform/ec2/ec2-plugin.rb +1 -0
- data/lib/boxgrinder-build/plugins/platform/ec2/src/rc_local +15 -8
- data/lib/boxgrinder-build/plugins/platform/vmware/vmware-plugin.rb +8 -6
- data/rubygem-boxgrinder-build.spec +23 -3
- data/spec/appliance-spec.rb +114 -73
- data/spec/helpers/guestfs-helper-spec.rb +12 -3
- data/spec/plugins/base-plugin-spec.rb +24 -18
- data/spec/plugins/delivery/ebs/ebs-plugin-spec.rb +206 -67
- data/spec/plugins/delivery/elastichosts/elastichosts-plugin-spec.rb +228 -225
- data/spec/plugins/delivery/local/local-plugin-spec.rb +13 -34
- data/spec/plugins/delivery/s3/s3-plugin-spec.rb +107 -50
- data/spec/plugins/os/centos/centos-plugin-spec.rb +1 -1
- data/spec/plugins/os/fedora/fedora-plugin-spec.rb +14 -8
- data/spec/plugins/os/rhel/rhel-plugin-spec.rb +1 -1
- data/spec/plugins/os/rpm-based/kickstart-spec.rb +0 -44
- data/spec/plugins/os/rpm-based/rpm-based-os-plugin-spec.rb +31 -2
- data/spec/plugins/os/rpm-based/rpm-dependency-validator-spec.rb +20 -7
- data/spec/plugins/os/sl/sl-plugin-spec.rb +44 -0
- data/spec/plugins/platform/ec2/ec2-plugin-spec.rb +1 -1
- data/spec/plugins/platform/virtualbox/virtualbox-plugin-spec.rb +6 -6
- data/spec/plugins/platform/vmware/vmware-plugin-spec.rb +14 -13
- metadata +27 -5
- 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
|
-
|
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
|
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
|
-
|
90
|
-
execute_plugin_chain
|
91
|
-
end
|
117
|
+
initialize_plugins
|
92
118
|
|
93
|
-
|
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
|
-
|
105
|
-
os_plugin.run(@appliance_definition)
|
106
|
-
@log.debug "Operating system plugin executed."
|
121
|
+
execute_plugin_chain
|
107
122
|
|
108
|
-
|
123
|
+
self
|
109
124
|
end
|
110
125
|
|
111
|
-
def
|
112
|
-
|
113
|
-
|
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
|
-
|
130
|
+
def delivery_selected?
|
131
|
+
!(@config.delivery == :none or @config.delivery.to_s.empty? == nil)
|
132
132
|
end
|
133
133
|
|
134
|
-
def
|
135
|
-
if
|
136
|
-
@log.
|
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
|
-
|
140
|
+
@log.debug "Executing #{plugin.plugin_info[:type]} plugin for #{@appliance_config.os.name}..."
|
141
141
|
|
142
|
-
|
143
|
-
|
144
|
-
|
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 '
|
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 =
|
41
|
-
rescue
|
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
|
-
|
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(
|
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
|
@@ -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(
|
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(
|
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
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
55
|
-
|
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
|
59
|
-
|
97
|
+
def after_init
|
98
|
+
@region = availability_zone_to_region(@current_availability_zone)
|
60
99
|
|
61
|
-
|
62
|
-
|
63
|
-
|
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'],
|
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
|
-
|
115
|
+
ami_info = ami_info(ebs_appliance_name)
|
72
116
|
|
73
|
-
if
|
74
|
-
@log.
|
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
|
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
|
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
|
-
|
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
|
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 =>
|
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
|
194
|
-
images = @ec2.describe_images(:owner_id => @plugin_config['account_number'].to_s.gsub(/-/,
|
195
|
-
|
196
|
-
|
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
|
-
|
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
|
209
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
244
|
-
|
245
|
-
|
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"
|