cloud-maker 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/cloud-maker +90 -0
- data/lib/cloud-maker.rb +4 -0
- data/lib/cloud_maker/config.rb +302 -0
- data/lib/cloud_maker/ec2.rb +80 -0
- data/lib/cloud_maker/s3_archiver.rb +72 -0
- data/lib/cloud_maker/shell_executor.rb +38 -0
- metadata +100 -0
data/bin/cloud-maker
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'thor'
|
3
|
+
require 'colorize'
|
4
|
+
require 'cloud-maker'
|
5
|
+
# require 'pry'
|
6
|
+
|
7
|
+
class CloudMakerCLI < Thor
|
8
|
+
include Thor::Actions
|
9
|
+
|
10
|
+
desc "launch [INSTANCE_CONFIG_YAML]", "Launch a new EC2 instance as described by INSTANCE_CONFIG_YAML"
|
11
|
+
method_option :aws_access_key_id,
|
12
|
+
:desc => "Your AWS access key id",
|
13
|
+
:default => ENV['AWS_ACCESS_KEY_ID'],
|
14
|
+
:required => true
|
15
|
+
method_option :aws_secret_access_key,
|
16
|
+
:desc => "Your AWS secret access key",
|
17
|
+
:default => ENV['AWS_SECRET_ACCESS_KEY'],
|
18
|
+
:required => true
|
19
|
+
method_option :set,
|
20
|
+
:alias => '-s',
|
21
|
+
:type => :hash,
|
22
|
+
:default => {},
|
23
|
+
:desc => "Set parameters in the CloudMaker config"
|
24
|
+
def launch(instance_config_yaml)
|
25
|
+
puts "---------------------------"
|
26
|
+
puts "Launching new EC2 instance"
|
27
|
+
puts "---------------------------\n"
|
28
|
+
|
29
|
+
config = CloudMaker::Config.from_yaml(instance_config_yaml)
|
30
|
+
options.set.each_pair {|key, val| config[key] = val}
|
31
|
+
|
32
|
+
if !config.valid?
|
33
|
+
say "Before an instance can be launched we need a few more values to be specified.".yellow
|
34
|
+
say "Currently missing: #{config.missing_values.map{|key| key.cyan}.join(', ')}"
|
35
|
+
puts
|
36
|
+
|
37
|
+
config.missing_values.each do |key|
|
38
|
+
config[key] = ENV[key] if ENV[key]
|
39
|
+
end
|
40
|
+
|
41
|
+
config.missing_values.each do |key|
|
42
|
+
if (config.options[key]["description"])
|
43
|
+
say config.options[key]["description"]
|
44
|
+
end
|
45
|
+
config[key] = ask "Please choose a value for #{key.cyan}: "
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
cloud_maker = CloudMaker::Ec2.new(config,
|
50
|
+
:aws_access_key_id => options.aws_access_key_id,
|
51
|
+
:aws_secret_access_key => options.aws_secret_access_key
|
52
|
+
)
|
53
|
+
|
54
|
+
if (!config.includes.empty?)
|
55
|
+
puts
|
56
|
+
say 'Include URLs:'.green
|
57
|
+
config.includes.each do |url|
|
58
|
+
puts url
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
puts
|
63
|
+
say "CloudMaker configuration:".green
|
64
|
+
print_table config.to_hash.map {|key, val| [key.dup.cyan, val]}
|
65
|
+
|
66
|
+
puts
|
67
|
+
say "Configuration file:".green + " " + config.extra_options[:config_path]
|
68
|
+
puts
|
69
|
+
|
70
|
+
if yes?("Launch a new EC2 instance with the options above? (y/n)")
|
71
|
+
instance = cloud_maker.launch
|
72
|
+
puts
|
73
|
+
say "Successfully launched new EC2 instance: ".green + instance[:aws_instance_id].magenta
|
74
|
+
puts
|
75
|
+
print_table instance.map {|key, val|
|
76
|
+
[key.to_s.cyan, val.to_s]
|
77
|
+
}
|
78
|
+
else
|
79
|
+
say "Launch aborted!".red
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "scratch", "Play with Thor"
|
84
|
+
def scratch(arg1)
|
85
|
+
say 'hello'
|
86
|
+
say 'hello in red'.red
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
CloudMakerCLI.start
|
data/lib/cloud-maker.rb
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module CloudMaker
|
5
|
+
class Config
|
6
|
+
# Public: Gets/Sets the CloudMaker specific properties. The options hash
|
7
|
+
# is formatted as:
|
8
|
+
# {
|
9
|
+
# 'key1' => {
|
10
|
+
# 'environment': boolean,
|
11
|
+
# 'required': boolean,
|
12
|
+
# 'value': value
|
13
|
+
# },
|
14
|
+
# 'key2' => { ... },
|
15
|
+
# ...
|
16
|
+
# }
|
17
|
+
attr_accessor :options
|
18
|
+
# Public: Gets/Sets the Hash of Cloud Init properties. See
|
19
|
+
# https://help.ubuntu.com/community/CloudInit for valid options
|
20
|
+
attr_accessor :cloud_config
|
21
|
+
# Public: Gets/Sets an Array of URLs to be included, this corresponds to the
|
22
|
+
# list of URLs in a Cloud Init includes file.
|
23
|
+
attr_accessor :includes
|
24
|
+
# Public: Gets/Sets extra information about the config to be stored for
|
25
|
+
# archival purposes.
|
26
|
+
attr_accessor :extra_options
|
27
|
+
|
28
|
+
# Internal: A mime header for the Cloud Init config section of the user data
|
29
|
+
CLOUD_CONFIG_HEADER = %Q|Content-Type: text/cloud-config; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="cloud-config.yaml"\n\n|
|
30
|
+
# Internal: A mime header for the includes section of the user data
|
31
|
+
INCLUDES_HEADER = %Q|Content-Type: text/x-include-url; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="includes.txt"\n\n|
|
32
|
+
# Internal: A mime header for the Cloud Boothook section of the user data
|
33
|
+
BOOTHOOK_HEADER = %Q|Content-Type: text/cloud-boothook; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="boothook.sh"\n\n|
|
34
|
+
# Internal: A multipart mime header for describing the entire user data
|
35
|
+
# content. It includes a placeholder for the boundary text '___boundary___'
|
36
|
+
# that needs to be replaced in the actual mime document.
|
37
|
+
MULTIPART_HEADER = %Q|Content-Type: multipart/mixed; boundary="___boundary___"\nMIME-Version: 1.0\n\n|
|
38
|
+
|
39
|
+
# Internal: If you don't specify a property associated with a key in the
|
40
|
+
# cloud_maker config file we will use these properties to fill in the blanks
|
41
|
+
DEFAULT_KEY_PROPERTIES = {
|
42
|
+
"environment" => true,
|
43
|
+
"required" => false,
|
44
|
+
"value" => nil
|
45
|
+
}
|
46
|
+
|
47
|
+
# Public: Initializes a new CloudMaker object.
|
48
|
+
#
|
49
|
+
# cloud_config - A Hash describing all properties of the CloudMaker config.
|
50
|
+
# 'cloud_maker' - The configuration properties for CloudMaker. These can
|
51
|
+
# be specified either as
|
52
|
+
# key: value
|
53
|
+
# or as
|
54
|
+
# key: {
|
55
|
+
# environment: boolean,
|
56
|
+
# required: boolean,
|
57
|
+
# value: value
|
58
|
+
# }
|
59
|
+
# If specified as key: value DEFAULT_KEY_PROPERTIES will
|
60
|
+
# be used. If the detailed version is used all properties
|
61
|
+
# are optional and DEFAULT_KEY_PROPERTIES will be used to
|
62
|
+
# fill in the blanks.
|
63
|
+
#
|
64
|
+
# 'include' - An array of URLs or a String containing 1 URL per line
|
65
|
+
# with optional # prefixed lines as comments.
|
66
|
+
# ... - All valid properties of a Cloud Init config
|
67
|
+
# are also valid here. See:
|
68
|
+
# https://help.ubuntu.com/community/CloudInit
|
69
|
+
# extra_options - Extra information about the instantiation. These will not
|
70
|
+
# be used to launch the instance but will be stored in the
|
71
|
+
# archive describing the instance.
|
72
|
+
#
|
73
|
+
# Returns a CloudMaker object
|
74
|
+
def initialize(cloud_config, extra_options)
|
75
|
+
self.extra_options = extra_options
|
76
|
+
cloud_config = cloud_config.dup
|
77
|
+
self.options = extract_cloudmaker_config!(cloud_config)
|
78
|
+
self.includes = extract_includes!(cloud_config)
|
79
|
+
self.cloud_config = cloud_config
|
80
|
+
end
|
81
|
+
|
82
|
+
# Public: Check if the CloudMaker config is in a valid state.
|
83
|
+
#
|
84
|
+
# Returns true if and only if all required properties have non-nil values
|
85
|
+
# and false otherwise.
|
86
|
+
def valid?
|
87
|
+
self.options.all? {|key, option| !(option["required"] && option["value"].nil?)}
|
88
|
+
end
|
89
|
+
|
90
|
+
# Public: Finds a list of keys in the CloudMaker config that are required to
|
91
|
+
# have a value but do not yet have one.
|
92
|
+
#
|
93
|
+
# Returns an Array of required keys that are missing values.
|
94
|
+
def missing_values
|
95
|
+
self.options.select {|key, option| option["required"] && option["value"].nil?}.map(&:first).map(&:dup)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns a Hash of all of the CloudMaker specific properties in the configuration.
|
99
|
+
def to_hash
|
100
|
+
self.options.map {|key, properties| [key, properties["value"]]}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Public: Generates a multipart userdata string suitable for use with Cloud Init on EC2
|
104
|
+
#
|
105
|
+
# Returns a String containing the mime encoded userdata
|
106
|
+
def to_user_data
|
107
|
+
# build a multipart document
|
108
|
+
parts = []
|
109
|
+
|
110
|
+
parts.push(BOOTHOOK_HEADER + boothook_data)
|
111
|
+
parts.push(CLOUD_CONFIG_HEADER + cloud_config_data)
|
112
|
+
parts.push(INCLUDES_HEADER + includes_data)
|
113
|
+
|
114
|
+
#not that it's likely but lets make sure that we don't choose a boundary that exists in the document.
|
115
|
+
boundary = ''
|
116
|
+
while parts.any? {|part| part.index(boundary)}
|
117
|
+
boundary = "===============#{rand(8999999999999999999) + 1000000000000000000}=="
|
118
|
+
end
|
119
|
+
|
120
|
+
header = MULTIPART_HEADER.sub(/___boundary___/, boundary)
|
121
|
+
|
122
|
+
return [header, *parts].join("\n--#{boundary}\n") + "\n--#{boundary}--"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Public: Generates a cloud-init configuration
|
126
|
+
#
|
127
|
+
# Returns a String containing the cloud init configuration in YAML format
|
128
|
+
def cloud_config_data
|
129
|
+
return "#cloud-config\n#{self.cloud_config.to_yaml}"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Public: Generates a shell script to set environment variables for all
|
133
|
+
# CloudMaker config options, will get executed at machine boot.
|
134
|
+
#
|
135
|
+
# Returns a String containing the shell script
|
136
|
+
def boothook_data
|
137
|
+
env_run_cmds = []
|
138
|
+
|
139
|
+
self.options.each_pair do |key, properties|
|
140
|
+
if properties["environment"] && !properties["value"].nil?
|
141
|
+
env_run_cmds.push(set_environment_variable_cmd(key, properties["value"]))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
return "#cloud-boothook\n#!/bin/sh\n#{env_run_cmds.join("\n")}\n"
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Public: Generates a cloud-init includes list
|
150
|
+
#
|
151
|
+
# Returns a String containing the cloud init includes list
|
152
|
+
def includes_data
|
153
|
+
["#include", *self.includes.map(&:to_s)].join("\n")
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
# Public: Access values in the cloudmaker options object
|
158
|
+
#
|
159
|
+
# key - The key of the property you're accessing
|
160
|
+
#
|
161
|
+
# Returns the value property for options[key]
|
162
|
+
def [](key)
|
163
|
+
self.options[key] ? self.options[key]["value"] : nil
|
164
|
+
end
|
165
|
+
|
166
|
+
# Public: Sets the value property for key in the cloudmaker options hash
|
167
|
+
#
|
168
|
+
# key - The key of the property you're accessing
|
169
|
+
# val - The value you wish to assign to the key
|
170
|
+
#
|
171
|
+
# Returns val
|
172
|
+
def []=(key, val)
|
173
|
+
if (self.options[key])
|
174
|
+
self.options[key]["value"] = val
|
175
|
+
else
|
176
|
+
self.options[key] = DEFAULT_KEY_PROPERTIES.merge('value' => val)
|
177
|
+
end
|
178
|
+
val
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns a String representation of the CloudMaker config
|
182
|
+
def inspect
|
183
|
+
"CloudMakerConfig#{self.options.inspect}"
|
184
|
+
end
|
185
|
+
|
186
|
+
class << self
|
187
|
+
|
188
|
+
# Public: Takes the path of a YAML file and loads a new Config object
|
189
|
+
# from it.
|
190
|
+
#
|
191
|
+
# instance_config_yaml - The path of the YAML file
|
192
|
+
#
|
193
|
+
# Returns a new Config
|
194
|
+
# Raises: Exception if the file doesn't exist.
|
195
|
+
# Raises: SyntaxError if the YAML file is invalid.
|
196
|
+
def from_yaml(instance_config_yaml)
|
197
|
+
begin
|
198
|
+
full_path = File.expand_path(instance_config_yaml)
|
199
|
+
cloud_yaml = File.open(full_path, "r") #Right_AWS will base64 encode this for us
|
200
|
+
rescue
|
201
|
+
raise "ERROR: The path to the CloudMaker config is incorrect"
|
202
|
+
end
|
203
|
+
|
204
|
+
CloudMaker::Config.new(YAML::load(cloud_yaml), :config_path => full_path)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
# Internal: Takes a CloudMaker config and parses out the CloudMaker
|
213
|
+
# specific portions of it. For each key/value it fills in any property
|
214
|
+
# blanks from the DEFAULT_KEY_PROPERTIES. It also deletes the cloud_maker
|
215
|
+
# property from config.
|
216
|
+
#
|
217
|
+
# config - A hash that should contain a 'cloud_maker' key storing CloudMaker
|
218
|
+
# configuration properties.
|
219
|
+
#
|
220
|
+
# Returns a Hash in the format of
|
221
|
+
# {'key1' => {
|
222
|
+
# 'environment' => ...,
|
223
|
+
# 'value' => ...,
|
224
|
+
# 'required' ...
|
225
|
+
# }, 'key2' => ... , ...}
|
226
|
+
def extract_cloudmaker_config!(config)
|
227
|
+
cloud_maker_config = config.delete('cloud-maker') || {}
|
228
|
+
cloud_maker_config.keys.each do |key|
|
229
|
+
#if key is set to anything but a hash then we treat it as the value property
|
230
|
+
if !advanced_config?(cloud_maker_config[key])
|
231
|
+
cloud_maker_config[key] = {
|
232
|
+
"value" => cloud_maker_config[key]
|
233
|
+
}
|
234
|
+
end
|
235
|
+
|
236
|
+
cloud_maker_config[key] = DEFAULT_KEY_PROPERTIES.merge(cloud_maker_config[key])
|
237
|
+
end
|
238
|
+
cloud_maker_config
|
239
|
+
end
|
240
|
+
|
241
|
+
# Internal: Determines if value should be treated as a value or a property
|
242
|
+
# configuration hash. A property configuration hash would specify at least
|
243
|
+
# one of environment, value, or required.
|
244
|
+
#
|
245
|
+
# value - The value to evaluate
|
246
|
+
#
|
247
|
+
# Returns true if value is a property configuration, and false if it's just
|
248
|
+
# a value
|
249
|
+
def advanced_config?(value)
|
250
|
+
value.kind_of?(Hash) && !(DEFAULT_KEY_PROPERTIES.keys & value.keys).empty?
|
251
|
+
end
|
252
|
+
|
253
|
+
# Internal: Takes a CloudMaker config and parses out the includes list. If
|
254
|
+
# the list is an array it treats each entry as a URL. If it is a string it
|
255
|
+
# treats the string as the contents of a Cloud Init include file.
|
256
|
+
#
|
257
|
+
# config - A hash that should contain an 'include' key storing the include
|
258
|
+
# information.
|
259
|
+
#
|
260
|
+
# Returns an Array of URLs
|
261
|
+
def extract_includes!(config)
|
262
|
+
includes = config.delete('include')
|
263
|
+
|
264
|
+
#if we didn't specify it just use a blank array
|
265
|
+
if includes.nil?
|
266
|
+
includes = []
|
267
|
+
#if we passed something other than an array turn it into a string and split it up into urls
|
268
|
+
elsif !includes.kind_of?(Array)
|
269
|
+
includes = includes.to_s.split("\n")
|
270
|
+
includes.reject! {|line| line.strip[0] == "#" || line.strip.empty?}
|
271
|
+
end
|
272
|
+
|
273
|
+
includes
|
274
|
+
end
|
275
|
+
|
276
|
+
# Internal: Generates the shell command necessary to set an environment
|
277
|
+
# variable. It escapes the value but assumes there are no special characters
|
278
|
+
# in the key. If value is an array or a hash it generates an environment
|
279
|
+
# variable for each value in the array/hash with the key set to key_index.
|
280
|
+
#
|
281
|
+
# key - The key of the environment variable
|
282
|
+
# value - The value to set the key to
|
283
|
+
#
|
284
|
+
# Returns a string that can be executed to globally set the environment variable.
|
285
|
+
def set_environment_variable_cmd(key, value)
|
286
|
+
if (value.kind_of?(Hash))
|
287
|
+
value.keys.map { |hash_key|
|
288
|
+
set_environment_variable_cmd("#{key}_#{hash_key}", value[hash_key])
|
289
|
+
}.join(';')
|
290
|
+
elsif (value.kind_of?(Array))
|
291
|
+
strings = []
|
292
|
+
value.each_with_index { |arr_val, i|
|
293
|
+
strings.push(set_environment_variable_cmd("#{key}_#{i}", arr_val))
|
294
|
+
}
|
295
|
+
strings.join(';')
|
296
|
+
else
|
297
|
+
escaped_value = value.to_s.gsub(/"/, '\\\\\\\\\"')
|
298
|
+
"echo \"#{key}=\\\"#{escaped_value}\\\"\" >> /etc/environment"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
|
3
|
+
module CloudMaker
|
4
|
+
class Ec2
|
5
|
+
# Public: Gets/Sets the AWS access key.
|
6
|
+
attr_accessor :aws_secret_access_key
|
7
|
+
# Public: Gets/Sets the AWS secret.
|
8
|
+
attr_accessor :aws_access_key_id
|
9
|
+
# Public: Gets/Sets the CloudMaker::Config
|
10
|
+
attr_accessor :config
|
11
|
+
|
12
|
+
# Public: Creates a new Ec2 instance
|
13
|
+
#
|
14
|
+
# cloud_maker_config - A CloudMaker::Config object describing the instance
|
15
|
+
# to be managed.
|
16
|
+
# options - S3 configuration options
|
17
|
+
# :aws_access_key_id - (required) The AWS access key
|
18
|
+
# :aws_secret_access_key - (required) The AWS secret
|
19
|
+
#
|
20
|
+
# Returns a new CloudMaker::Ec2 instance
|
21
|
+
# Raises RuntimeError if any of the required options are not specified
|
22
|
+
def initialize(cloud_maker_config, options)
|
23
|
+
required_keys = [:aws_access_key_id, :aws_secret_access_key]
|
24
|
+
unless (required_keys - options.keys).empty?
|
25
|
+
raise RuntimeError.new("Instantiated #{self.class} without required attributes: #{required_keys - options.keys}.")
|
26
|
+
end
|
27
|
+
|
28
|
+
self.config = cloud_maker_config
|
29
|
+
self.aws_access_key_id = options[:aws_access_key_id]
|
30
|
+
self.aws_secret_access_key = options[:aws_secret_access_key]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Launches a new EC2 instance, associates any specified elastic IPS
|
34
|
+
# with it, adds any specified tags, and archives the launch details to S3.
|
35
|
+
#
|
36
|
+
# Returns a RightAws supplied Hash describing the launched instance.
|
37
|
+
def launch
|
38
|
+
ec2 = RightAws::Ec2.new(self.aws_access_key_id, self.aws_secret_access_key)
|
39
|
+
|
40
|
+
user_data = self.config.to_user_data
|
41
|
+
|
42
|
+
instance = ec2.launch_instances(config['ami'],
|
43
|
+
:group_names => config['security_group'],
|
44
|
+
:instance_type => config['instance_type'],
|
45
|
+
:key_name => config['key_pair'],
|
46
|
+
:user_data => user_data
|
47
|
+
).first
|
48
|
+
|
49
|
+
instance_id = instance[:aws_instance_id]
|
50
|
+
|
51
|
+
puts "Launched instance: #{instance_id.to_s.on_light_blue}"
|
52
|
+
|
53
|
+
ec2.create_tags(instance_id, self.config["tags"]) if self.config["tags"]
|
54
|
+
|
55
|
+
if (self.config["elastic_ip"])
|
56
|
+
#we can't associate IPs while the state is pending
|
57
|
+
while instance[:aws_state] == 'pending'
|
58
|
+
print '.'
|
59
|
+
STDOUT.flush
|
60
|
+
#this is going to hammer EC2 a bit, it might be necessary to add some delay in here
|
61
|
+
instance = ec2.describe_instances([instance_id]).first
|
62
|
+
end
|
63
|
+
|
64
|
+
ec2.associate_address(instance_id, :public_ip => self.config["elastic_ip"])
|
65
|
+
|
66
|
+
instance = ec2.describe_instances([instance_id]).first # So we get the correct IP address
|
67
|
+
end
|
68
|
+
|
69
|
+
archiver = S3Archiver.new(
|
70
|
+
:instance_id => instance_id,
|
71
|
+
:aws_access_key_id => self.aws_access_key_id,
|
72
|
+
:aws_secret_access_key => self.aws_secret_access_key,
|
73
|
+
:bucket_name => self.config["s3_archive_bucket"]
|
74
|
+
)
|
75
|
+
archiver.store_archive(self.config, instance)
|
76
|
+
|
77
|
+
instance
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
|
3
|
+
module CloudMaker
|
4
|
+
class S3Archiver
|
5
|
+
|
6
|
+
# Public: Gets/Sets the AWS access key.
|
7
|
+
attr_accessor :aws_secret_access_key
|
8
|
+
# Public: Gets/Sets the AWS secret.
|
9
|
+
attr_accessor :aws_access_key_id
|
10
|
+
# Public: Gets/Sets the EC2 instance ID string.
|
11
|
+
attr_accessor :instance_id
|
12
|
+
# Internal: Gets/Sets the RightAws::S3::Bucket used for storing/loading archives.
|
13
|
+
attr_accessor :bucket
|
14
|
+
|
15
|
+
# Public: All archive keys will be prefixed with KEY_PREFIX/
|
16
|
+
KEY_PREFIX = "cloud-maker"
|
17
|
+
|
18
|
+
# Public: Creates a new S3 Archiver instance
|
19
|
+
#
|
20
|
+
# options - S3 configuration options
|
21
|
+
# :aws_access_key_id - (required) The AWS access key
|
22
|
+
# :aws_secret_access_key - (required) The AWS secret
|
23
|
+
# :bucket_name - (required) The bucket for the archiver to access
|
24
|
+
# :instance_id - (required) The AWS instance ID the archive describes
|
25
|
+
#
|
26
|
+
# Returns a new CloudMaker::S3Archiver instance
|
27
|
+
# Raises RuntimeError if any of the required options are not specified
|
28
|
+
def initialize(options)
|
29
|
+
required_keys = [:aws_access_key_id, :aws_secret_access_key, :instance_id, :bucket_name]
|
30
|
+
unless (required_keys - options.keys).empty?
|
31
|
+
raise RuntimeError.new("Instantiated #{self.class} without required attributes: #{required_keys - options.keys}.")
|
32
|
+
end
|
33
|
+
|
34
|
+
self.instance_id = options[:instance_id]
|
35
|
+
self.aws_access_key_id = options[:aws_access_key_id]
|
36
|
+
self.aws_secret_access_key = options[:aws_secret_access_key]
|
37
|
+
|
38
|
+
s3 = RightAws::S3.new(self.aws_access_key_id, self.aws_secret_access_key)
|
39
|
+
self.bucket = s3.bucket(options[:bucket_name])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Generates an archive with all information relevant to an instance
|
43
|
+
# launch and stores it to S3.
|
44
|
+
#
|
45
|
+
# cloud_maker_config - The CloudMaker::Config the instance was launched with
|
46
|
+
# instance - A Hash describing the properties of the launched instance
|
47
|
+
#
|
48
|
+
# Returns nothing.
|
49
|
+
def store_archive(cloud_maker_config, instance)
|
50
|
+
userdata = cloud_maker_config.to_user_data
|
51
|
+
self.bucket.put(self.key + "/user_data.cloud_config", userdata)
|
52
|
+
self.bucket.put(self.key + "/instance.yaml", instance.to_yaml)
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Retrieves a previously created archive from S3
|
57
|
+
#
|
58
|
+
# Returns the content of the archive.
|
59
|
+
def load_archive
|
60
|
+
self.bucket.get(self.key)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public: Returns the key that the archive will be stored under
|
64
|
+
def key
|
65
|
+
if self.instance_id
|
66
|
+
[KEY_PREFIX, self.instance_id].join('/')
|
67
|
+
else
|
68
|
+
raise RuntimeError.new("Attempted to generate a key name without an instance id.")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module CloudMaker
|
2
|
+
class ShellExecutor
|
3
|
+
YAML_DOMAIN = "!airbnb.com,2012-07-19"
|
4
|
+
YAML_TYPE = "shell-script"
|
5
|
+
|
6
|
+
attr_accessor :script
|
7
|
+
|
8
|
+
def initialize(script)
|
9
|
+
self.script = script
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_yaml_type
|
13
|
+
"#{YAML_DOMAIN}/#{YAML_TYPE}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_yaml(opts = {})
|
17
|
+
YAML.quick_emit( nil, opts ) { |out|
|
18
|
+
out.scalar( "tag:yaml.org,2002:str", execute, :plain )
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute
|
23
|
+
@result ||= `#{self.script}`.chomp
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
execute
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_string_representation(string_representation)
|
31
|
+
ShellExecutor.new(string_representation)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
YAML::add_domain_type(CloudMaker::ShellExecutor::YAML_DOMAIN, CloudMaker::ShellExecutor::YAML_TYPE) do |type, val|
|
37
|
+
CloudMaker::ShellExecutor.from_string_representation(val)
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloud-maker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Nathan Baxter
|
9
|
+
- Flo Leibert
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-07-20 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: colorize
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: thor
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0.15'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.15'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: right_aws
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.0'
|
63
|
+
description:
|
64
|
+
email: nathan.baxter@airbnb.cloud-maker
|
65
|
+
executables:
|
66
|
+
- cloud-maker
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- lib/cloud-maker.rb
|
71
|
+
- lib/cloud_maker/config.rb
|
72
|
+
- lib/cloud_maker/ec2.rb
|
73
|
+
- lib/cloud_maker/s3_archiver.rb
|
74
|
+
- lib/cloud_maker/shell_executor.rb
|
75
|
+
- bin/cloud-maker
|
76
|
+
homepage: https://github.com/airbnb/cloud-maker
|
77
|
+
licenses: []
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.8.24
|
97
|
+
signing_key:
|
98
|
+
specification_version: 3
|
99
|
+
summary: Launch and perform initial configuration of cloud servers.
|
100
|
+
test_files: []
|