CloudyScripts 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,95 +1,97 @@
1
- # Implements a little state-machine.
2
- # Usage: for every state you need, extend this class.
3
- # The method enter() must be implemented for every state you code and
4
- # return another state.
5
- class ScriptExecutionState
6
- # context information for the state (hash)
7
- attr_reader :context, :logger
8
-
9
- def initialize(context)
10
- @context = context
11
- @state_change_listeners = []
12
- @logger = context[:logger]
13
- if @logger == nil
14
- @logger = Logger.new(STDOUT)
15
- @logger.level = Logger::WARN
16
- end
17
- end
18
-
19
- # Listener should extend #StateChangeListener (or implement a
20
- # state_changed(state) method). Note: calls are synchronous.
21
- def register_state_change_listener(listener)
22
- @state_change_listeners << listener
23
- end
24
-
25
- # Start the state machine using this state as initial state.
26
- def start_state_machine
27
- @current_state = self
28
- @logger.info "start state machine with #{@current_state.inspect}"
29
- while !@current_state.done? && !@current_state.failed?
30
- begin
31
- @logger.info "state machine: current state = #{@current_state.inspect}"
32
- @current_state = @current_state.enter()
33
- notify_state_change_listeners(@current_state)
34
- rescue Exception => e
35
- @context[:details] = e
36
- @current_state = FailedState.new(@context, e.to_s, @current_state)
37
- notify_state_change_listeners(@current_state)
38
- @logger.warn "Exception: #{e}"
39
- @logger.warn "#{e.backtrace.join("\n")}"
40
- end
41
- end
42
- @current_state
43
- end
44
-
45
- # Returns the state that is reached after execution.
46
- def end_state
47
- @current_state
48
- end
49
-
50
- # To be implemented. Executes the code for this state.
51
- def enter
52
- raise Exception.new("TaskExecutionState is abstract")
53
- end
54
-
55
- # To be implemented. Indicates if the final state is reached.
56
- def done?
57
- false
58
- end
59
-
60
- # To be implemented. Indicates if the final state is a failure state.
61
- def failed?
62
- false
63
- end
64
-
65
- def to_s
66
- s = self.class.to_s
67
- s.sub(/.*\:\:/,'')
68
- end
69
-
70
- # Standard state reached when an exception occurs.
71
- class FailedState < ScriptExecutionState
72
- attr_accessor :failure_reason, :from_state
73
- def initialize(context, failure_reason, from_state)
74
- super(context)
75
- @failure_reason = failure_reason
76
- @from_state = from_state
77
- end
78
- def done?
79
- true
80
- end
81
- def failed?
82
- true
83
- end
84
- end
85
-
86
- private
87
-
88
- # Notifies all listeners of state changes
89
- def notify_state_change_listeners(state)
90
- @state_change_listeners.each() {|listener|
91
- listener.state_changed(state)
92
- }
93
- end
94
-
95
- end
1
+ # Implements a little state-machine.
2
+ # Usage: for every state you need, extend this class.
3
+ # The method enter() must be implemented for every state you code and
4
+ # return another state.
5
+ class ScriptExecutionState
6
+ # context information for the state (hash)
7
+ attr_reader :context, :logger
8
+
9
+ def initialize(context)
10
+ @context = context
11
+ @state_change_listeners = []
12
+ @logger = context[:logger]
13
+ if @logger == nil
14
+ @logger = Logger.new(STDOUT)
15
+ @logger.level = Logger::WARN
16
+ end
17
+ end
18
+
19
+ # Listener should extend #StateChangeListener (or implement a
20
+ # state_changed(state) method). Note: calls are synchronous.
21
+ def register_state_change_listener(listener)
22
+ @state_change_listeners << listener
23
+ end
24
+
25
+ # Start the state machine using this state as initial state.
26
+ def start_state_machine
27
+ @current_state = self
28
+ @logger.info "start state machine with #{@current_state.to_s}"
29
+ while !@current_state.done? && !@current_state.failed?
30
+ begin
31
+ @logger.info "state machine: current state = #{@current_state.to_s}"
32
+ @current_state = @current_state.enter()
33
+ notify_state_change_listeners(@current_state)
34
+ rescue Exception => e
35
+ if @context[:result] != nil
36
+ @context[:result][:details] = e.backtrace().join("\n")
37
+ end
38
+ @current_state = FailedState.new(@context, e.to_s, @current_state)
39
+ notify_state_change_listeners(@current_state)
40
+ @logger.warn "StateMachine exception during execution: #{e}"
41
+ @logger.warn "#{e.backtrace.join("\n")}"
42
+ end
43
+ end
44
+ @current_state
45
+ end
46
+
47
+ # Returns the state that is reached after execution.
48
+ def end_state
49
+ @current_state
50
+ end
51
+
52
+ # To be implemented. Executes the code for this state.
53
+ def enter
54
+ raise Exception.new("TaskExecutionState is abstract")
55
+ end
56
+
57
+ # To be implemented. Indicates if the final state is reached.
58
+ def done?
59
+ false
60
+ end
61
+
62
+ # To be implemented. Indicates if the final state is a failure state.
63
+ def failed?
64
+ false
65
+ end
66
+
67
+ def to_s
68
+ s = self.class.to_s
69
+ s.sub(/.*\:\:/,'')
70
+ end
71
+
72
+ # Standard state reached when an exception occurs.
73
+ class FailedState < ScriptExecutionState
74
+ attr_accessor :failure_reason, :from_state
75
+ def initialize(context, failure_reason, from_state)
76
+ super(context)
77
+ @failure_reason = failure_reason
78
+ @from_state = from_state
79
+ end
80
+ def done?
81
+ true
82
+ end
83
+ def failed?
84
+ true
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ # Notifies all listeners of state changes
91
+ def notify_state_change_listeners(state)
92
+ @state_change_listeners.each() {|listener|
93
+ listener.state_changed(state)
94
+ }
95
+ end
96
+
97
+ end
@@ -1,13 +1,13 @@
1
- # Defines a template for a class that allows to be notified
2
- # on state-changes in #ScriptExecutionState. A listener must be
3
- # registered via #ScriptExecutionState::register_state_change_listener.
4
- # When a state changes the method #state_changed is called.
5
-
6
- class StateChangeListener
7
- # Method called when a state changes. Note: calls are synchronous, the
8
- # listener should return quickly and handle more complicated routines
9
- # in a different thread.
10
- def state_changed(state)
11
- raise Exception.new("StateChangeListener: state change notification not implemented")
12
- end
13
- end
1
+ # Defines a template for a class that allows to be notified
2
+ # on state-changes in #ScriptExecutionState. A listener must be
3
+ # registered via #ScriptExecutionState::register_state_change_listener.
4
+ # When a state changes the method #state_changed is called.
5
+
6
+ class StateChangeListener
7
+ # Method called when a state changes. Note: calls are synchronous, the
8
+ # listener should return quickly and handle more complicated routines
9
+ # in a different thread.
10
+ def state_changed(state)
11
+ raise Exception.new("StateChangeListener: state change notification not implemented")
12
+ end
13
+ end
@@ -1,442 +1,446 @@
1
- require "help/script_execution_state"
2
- require "scripts/ec2/ec2_script"
3
- require "help/remote_command_handler"
4
- #require "help/dm_crypt_helper"
5
- require "AWS"
6
-
7
- class AWS::EC2::Base
8
- def register_image_updated(options)
9
- params = {}
10
- params["Name"] = options[:name].to_s
11
- params["BlockDeviceMapping.1.Ebs.SnapshotId"] = options[:snapshot_id].to_s
12
- params["BlockDeviceMapping.1.DeviceName"] = options[:root_device_name].to_s
13
- params["Description"] = options[:description].to_s
14
- params["KernelId"] = options[:kernel_id].to_s
15
- params["RamdiskId"] = options[:ramdisk_id].to_s
16
- params["Architecture"] = options[:architecture].to_s
17
- params["RootDeviceName"] = options[:root_device_name].to_s
18
- return response_generator(:action => "RegisterImage", :params => params)
19
- end
20
- end
21
-
22
- # Creates a bootable EBS storage from an existing AMI.
23
- #
24
-
25
- class Ami2EbsConversion < Ec2Script
26
- # Input parameters
27
- # * aws_access_key => the Amazon AWS Access Key (see Your Account -> Security Credentials)
28
- # * aws_secret_key => the Amazon AWS Secret Key
29
- # * ami_id => the ID of the AMI to be converted
30
- # * security_group_name => name of the security group to start
31
- # * ssh_key_data => Key information for the security group that starts the AMI [if not set, use ssh_key_files]
32
- # * ssh_key_files => Key information for the security group that starts the AMI
33
- # * remote_command_handler => object that allows to connect via ssh and execute commands (optional)
34
- # * ec2_api_handler => object that allows to access the EC2 API (optional)
35
- # * ec2_api_server => server to connect to (option, default is us-east-1.ec2.amazonaws.com)
36
- # * name => the name of the AMI to be created
37
- # * description => description on AMI to be created (optional)
38
- # * temp_device_name => [default /dev/sdj] device name used to attach the temporary storage; change this only if there's already a volume attacged as /dev/sdj (optional, default is /dev/sdj)
39
- # * root_device_name"=> [default /dev/sda1] device name used for the root device (optional)
40
- def initialize(input_params)
41
- super(input_params)
42
- @result = {:done => false}
43
- end
44
-
45
- # Executes the script.
46
- def start_script()
47
- begin
48
- # optional parameters and initialization
49
- if @input_params[:name] == nil
50
- @input_params[:name] = "Boot EBS (for AMI #{@input_params[:ami_id]}) at #{Time.now.strftime('%d/%m/%Y %H.%M.%S')}"
51
- else
52
- end
53
- if @input_params[:description] == nil
54
- @input_params[:description] = @input_params[:name]
55
- end
56
- if @input_params[:temp_device_name] == nil
57
- @input_params[:temp_device_name] = "/dev/sdj"
58
- end
59
- if @input_params[:root_device_name] == nil
60
- @input_params[:root_device_name] = "/dev/sda1"
61
- end
62
- # start state machine
63
- current_state = Ami2EbsConversionState.load_state(@input_params)
64
- @state_change_listeners.each() {|listener|
65
- current_state.register_state_change_listener(listener)
66
- }
67
- end_state = current_state.start_state_machine()
68
- if end_state.failed?
69
- @result[:failed] = true
70
- @result[:failure_reason] = current_state.end_state.failure_reason
71
- @result[:end_state] = current_state.end_state
72
- else
73
- @result[:failed] = false
74
- end
75
- rescue Exception => e
76
- @logger.warn "exception during encryption: #{e}"
77
- @logger.warn e.backtrace.join("\n")
78
- err = e.to_s
79
- err += " (in #{current_state.end_state.to_s})" unless current_state == nil
80
- @result[:failed] = true
81
- @result[:failure_reason] = err
82
- @result[:end_state] = current_state.end_state unless current_state == nil
83
- ensure
84
- begin
85
- @input_params[:remote_command_handler].disconnect
86
- rescue Exception => e2
87
- end
88
- end
89
-
90
- #
91
- @result[:done] = true
92
- end
93
-
94
- # Returns a hash with the following information:
95
- # :done => if execution is done
96
- #
97
- def get_execution_result
98
- @result
99
- end
100
-
101
- private
102
-
103
- # Here begins the state machine implementation
104
- class Ami2EbsConversionState < ScriptExecutionState
105
- def self.load_state(context)
106
- state = context[:initial_state] == nil ? InitialState.new(context) : context[:initial_state]
107
- state
108
- end
109
-
110
- def connect
111
- if @context[:remote_command_handler] == nil
112
- @context[:remote_command_handler] = RemoteCommandHandler.new
113
- end
114
- connected = false
115
- remaining_trials = 3
116
- while !connected && remaining_trials > 0
117
- remaining_trials -= 1
118
- if @context[:ssh_keyfile] != nil
119
- begin
120
- @context[:remote_command_handler].connect_with_keyfile(@context[:dns_name], @context[:ssh_keyfile])
121
- connected = true
122
- rescue Exception => e
123
- @logger.info("connection failed due to #{e}")
124
- end
125
- elsif @context[:ssh_keydata] != nil
126
- begin
127
- @context[:remote_command_handler].connect(@context[:dns_name], "root", @context[:ssh_keydata])
128
- connected = true
129
- rescue Exception => e
130
- @logger.info("connection failed due to #{e}")
131
- end
132
- else
133
- raise Exception.new("no key information specified")
134
- end
135
- if !connected
136
- sleep(5) #try again
137
- end
138
- end
139
- @logger.info "connected to #{@context[:dns_name]}"
140
- end
141
-
142
- end
143
-
144
- # Nothing done yet. Start by instantiating an AMI (in the right zone?)
145
- # which serves to create
146
- class InitialState < Ami2EbsConversionState
147
- def enter
148
- startup_ami()
149
- end
150
-
151
- private
152
-
153
- def startup_ami()
154
- @logger.debug "start up AMI #{@context[:ami_id]}"
155
- res = @context[:ec2_api_handler].run_instances(:image_id => @context[:ami_id],
156
- :security_group => @context[:security_group_name], :key_name => @context[:key_name])
157
- instance_id = res['instancesSet']['item'][0]['instanceId']
158
- @context[:instance_id] = instance_id
159
- @logger.info "started instance #{instance_id}"
160
- #availability_zone , key_name/group_name
161
- started = false
162
- while started == false
163
- sleep(5)
164
- res = @context[:ec2_api_handler].describe_instances(:instance_id => @context[:instance_id])
165
- state = res['reservationSet']['item'][0]['instancesSet']['item'][0]['instanceState']
166
- @logger.info "instance in state #{state['name']} (#{state['code']})"
167
- if state['code'].to_i == 16
168
- started = true
169
- @context[:dns_name] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['dnsName']
170
- @context[:availability_zone] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['placement']['availabilityZone']
171
- @context[:kernel_id] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['kernelId']
172
- @context[:ramdisk_id] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['ramdiskId']
173
- @context[:architecture] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['architecture']
174
- elsif state['code'].to_i != 0
175
- raise Exception.new('instance failed to start up')
176
- end
177
- end
178
- AmiStarted.new(@context)
179
- end
180
- end
181
-
182
- # Ami started. Create a storage
183
- class AmiStarted < Ami2EbsConversionState
184
- def enter
185
- create_storage()
186
- end
187
-
188
- private
189
-
190
- def create_storage()
191
- @logger.debug "create volume in zone #{@context[:availability_zone]}"
192
- res = @context[:ec2_api_handler].create_volume(:availability_zone => @context[:availability_zone], :size => "10")
193
- @context[:volume_id] = res['volumeId']
194
- started = false
195
- while !started
196
- sleep(5)
197
- #TODO: check for timeout?
198
- res = @context[:ec2_api_handler].describe_volumes(:volume_id => @context[:volume_id])
199
- state = res['volumeSet']['item'][0]['status']
200
- @logger.debug "volume state #{state}"
201
- if state == 'available'
202
- started = true
203
- end
204
- end
205
- StorageCreated.new(@context)
206
- end
207
-
208
- end
209
-
210
- # Storage created. Attach it.
211
- class StorageCreated < Ami2EbsConversionState
212
- def enter
213
- attach_storage()
214
- end
215
-
216
- private
217
-
218
- def attach_storage()
219
- @logger.debug "attach volume #{@context[:volume_id]} to instance #{@context[:instance_id]} on device #{@context[:temp_device_name]}"
220
- @context[:ec2_api_handler].attach_volume(:volume_id => @context[:volume_id],
221
- :instance_id => @context[:instance_id],
222
- :device => @context[:temp_device_name]
223
- )
224
- done = false
225
- while !done
226
- sleep(5)
227
- #TODO: check for timeout?
228
- res = @context[:ec2_api_handler].describe_volumes(:volume_id => @context[:volume_id])
229
- state = res['volumeSet']['item'][0]['status']
230
- @logger.debug "storage attaching: #{state}"
231
- if state == 'in-use'
232
- done = true
233
- end
234
- end
235
- StorageAttached.new(@context)
236
- end
237
-
238
- end
239
-
240
- # Storage attached. Create a file-system and moun it
241
- class StorageAttached < Ami2EbsConversionState
242
- def enter
243
- create_fs()
244
- end
245
-
246
- private
247
-
248
- def create_fs()
249
- @logger.debug "create filesystem on #{@context[:dns_name]} to #{@context[:temp_device_name]}"
250
- connect()
251
- @context[:remote_command_handler].create_filesystem("ext3", @context[:temp_device_name])
252
- FileSystemCreated.new(@context)
253
- end
254
- end
255
-
256
- # File system created. Mount it.
257
- class FileSystemCreated < Ami2EbsConversionState
258
- def enter
259
- mount_fs()
260
- end
261
-
262
- private
263
-
264
- def mount_fs()
265
- @context[:path] = "/mnt/tmp_#{@context[:volume_id]}"
266
- @logger.debug "mount #{@context[:temp_device_name]} on #{@context[:path]}"
267
- @context[:remote_command_handler].mkdir(@context[:path])
268
- @context[:remote_command_handler].mount(@context[:temp_device_name], @context[:path])
269
- sleep(2) #give mount some time
270
- if !@context[:remote_command_handler].drive_mounted?(@context[:path])
271
- raise Exception.new("drive #{@context[:path]} not mounted")
272
- end
273
- FileSystemMounted.new(@context)
274
- end
275
- end
276
-
277
- # File system created and mounted. Copy the root partition.
278
- class FileSystemMounted < Ami2EbsConversionState
279
- def enter
280
- copy()
281
- end
282
-
283
- private
284
-
285
- def copy()
286
- @logger.debug "start copying to #{@context[:path]}"
287
- start = Time.new.to_i
288
- @context[:remote_command_handler].rsync("/", "#{@context[:path]}")
289
- @context[:remote_command_handler].rsync("/dev/", "#{@context[:path]}/dev/")
290
- endtime = Time.new.to_i
291
- @logger.info "copy took #{(endtime-start)}s"
292
- CopyDone.new(@context)
293
- end
294
- end
295
-
296
- # Copy operation done. Unmount volume.
297
- class CopyDone < Ami2EbsConversionState
298
- def enter
299
- unmount()
300
- end
301
-
302
- private
303
-
304
- def unmount()
305
- @logger.debug "unmount #{@context[:path]}"
306
- @context[:remote_command_handler].umount(@context[:path])
307
- sleep(2) #give umount some time
308
- if @context[:remote_command_handler].drive_mounted?(@context[:path])
309
- raise Exception.new("drive #{@context[:path]} not unmounted")
310
- end
311
- VolumeUnmounted.new(@context)
312
- end
313
- end
314
-
315
- # Volume unmounted. Detach it.
316
- class VolumeUnmounted < Ami2EbsConversionState
317
- def enter
318
- detach()
319
- end
320
-
321
- private
322
-
323
- def detach()
324
- @logger.debug "detach volume #{@context[:volume_id]}"
325
- @context[:ec2_api_handler].detach_volume(:volume_id => @context[:volume_id],
326
- :instance_id => @context[:instance_id]
327
- )
328
- done = false
329
- while !done
330
- sleep(3)
331
- #TODO: check for timeout?
332
- res = @context[:ec2_api_handler].describe_volumes(:volume_id => @context[:volume_id])
333
- @logger.debug "volume detaching: #{res.inspect}"
334
- if res['volumeSet']['item'][0]['status'] == 'available'
335
- done = true
336
- end
337
- end
338
- VolumeDetached.new(@context)
339
- end
340
- end
341
-
342
-
343
- # VolumeDetached. Create snaphot
344
- class VolumeDetached < Ami2EbsConversionState
345
- def enter
346
- create_snapshot()
347
- end
348
-
349
- private
350
-
351
- def create_snapshot()
352
- @logger.debug "create snapshot for volume #{@context[:volume_id]}"
353
- res = @context[:ec2_api_handler].create_snapshot(:volume_id => @context[:volume_id])
354
- @context[:snapshot_id] = res['snapshotId']
355
- @logger.info "snapshot_id = #{@context[:snapshot_id]}"
356
- done = false
357
- while !done
358
- sleep(5)
359
- #TODO: check for timeout?
360
- res = @context[:ec2_api_handler].describe_snapshots(:snapshot_id => @context[:snapshot_id])
361
- @logger.debug "snapshot creating: #{res.inspect}"
362
- if res['snapshotSet']['item'][0]['status'] == 'completed'
363
- done = true
364
- end
365
- end
366
- SnapshotCreated.new(@context)
367
- end
368
- end
369
-
370
- # Snapshot created. Delete volume.
371
- class SnapshotCreated < Ami2EbsConversionState
372
- def enter
373
- delete_volume()
374
- end
375
-
376
- private
377
-
378
- def delete_volume
379
- @logger.debug "delete volume #{@context[:volume_id]}"
380
- res = @context[:ec2_api_handler].delete_volume(:volume_id => @context[:volume_id])
381
- VolumeDeleted.new(@context)
382
- end
383
- end
384
-
385
- # Volume deleted. Register snapshot.
386
- class VolumeDeleted < Ami2EbsConversionState
387
- def enter
388
- register()
389
- end
390
-
391
- private
392
-
393
- def register()
394
- @logger.debug "register snapshot #{@context[:snapshot_id]} as #{@context[:name]}"
395
- res = @context[:ec2_api_handler].register_image_updated(:snapshot_id => @context[:snapshot_id],
396
- :kernel_id => @context[:kernel_id], :architecture => @context[:architecture],
397
- :root_device_name => @context[:root_device_name],
398
- :description => @context[:description], :name => @context[:name],
399
- :ramdisk_id => @context[:ramdisk_id]
400
- )
401
- @logger.debug "result of registration = #{res.inspect}"
402
- @context[:image_id] = res['imageId']
403
- @logger.info "resulting image_id = #{@context[:image_id]}"
404
- SnapshotRegistered.new(@context)
405
- end
406
- end
407
-
408
- # Snapshot registered. Shutdown instance.
409
- class SnapshotRegistered < Ami2EbsConversionState
410
- def enter
411
- shut_down()
412
- end
413
-
414
- private
415
-
416
- def shut_down()
417
- @logger.debug "shutdown instance #{@context[:instance_id]}"
418
- res = @context[:ec2_api_handler].terminate_instances(:instance_id => @context[:instance_id])
419
- done = false
420
- while done == false
421
- sleep(5)
422
- res = @context[:ec2_api_handler].describe_instances(:instance_id => @context[:instance_id])
423
- state = res['reservationSet']['item'][0]['instancesSet']['item'][0]['instanceState']
424
- @logger.debug "instance in state #{state['name']} (#{state['code']})"
425
- if state['code'].to_i == 48
426
- done = true
427
- elsif state['code'].to_i != 32
428
- raise Exception.new('instance failed to shut down')
429
- end
430
- end
431
- Done.new(@context)
432
- end
433
- end
434
-
435
- # Instance shutdown. Done.
436
- class Done < Ami2EbsConversionState
437
- def done?
438
- true
439
- end
440
- end
441
-
442
- end
1
+ require "help/script_execution_state"
2
+ require "scripts/ec2/ec2_script"
3
+ require "help/remote_command_handler"
4
+ #require "help/dm_crypt_helper"
5
+ require "AWS"
6
+
7
+ class AWS::EC2::Base
8
+ def register_image_updated(options)
9
+ params = {}
10
+ params["Name"] = options[:name].to_s
11
+ params["BlockDeviceMapping.1.Ebs.SnapshotId"] = options[:snapshot_id].to_s
12
+ params["BlockDeviceMapping.1.DeviceName"] = options[:root_device_name].to_s
13
+ params["Description"] = options[:description].to_s
14
+ params["KernelId"] = options[:kernel_id].to_s
15
+ params["RamdiskId"] = options[:ramdisk_id].to_s
16
+ params["Architecture"] = options[:architecture].to_s
17
+ params["RootDeviceName"] = options[:root_device_name].to_s
18
+ return response_generator(:action => "RegisterImage", :params => params)
19
+ end
20
+ end
21
+
22
+ # Creates a bootable EBS storage from an existing AMI.
23
+ #
24
+
25
+ class Ami2EbsConversion < Ec2Script
26
+ # Input parameters
27
+ # * aws_access_key => the Amazon AWS Access Key (see Your Account -> Security Credentials)
28
+ # * aws_secret_key => the Amazon AWS Secret Key
29
+ # * ami_id => the ID of the AMI to be converted
30
+ # * security_group_name => name of the security group to start
31
+ # * ssh_key_data => Key information for the security group that starts the AMI [if not set, use ssh_key_files]
32
+ # * ssh_key_files => Key information for the security group that starts the AMI
33
+ # * remote_command_handler => object that allows to connect via ssh and execute commands (optional)
34
+ # * ec2_api_handler => object that allows to access the EC2 API (optional)
35
+ # * ec2_api_server => server to connect to (option, default is us-east-1.ec2.amazonaws.com)
36
+ # * name => the name of the AMI to be created
37
+ # * description => description on AMI to be created (optional)
38
+ # * temp_device_name => [default /dev/sdj] device name used to attach the temporary storage; change this only if there's already a volume attacged as /dev/sdj (optional, default is /dev/sdj)
39
+ # * root_device_name"=> [default /dev/sda1] device name used for the root device (optional)
40
+ def initialize(input_params)
41
+ super(input_params)
42
+ end
43
+
44
+ # Executes the script.
45
+ def start_script()
46
+ begin
47
+ # optional parameters and initialization
48
+ if @input_params[:name] == nil
49
+ @input_params[:name] = "Boot EBS (for AMI #{@input_params[:ami_id]}) at #{Time.now.strftime('%d/%m/%Y %H.%M.%S')}"
50
+ else
51
+ end
52
+ if @input_params[:description] == nil
53
+ @input_params[:description] = @input_params[:name]
54
+ end
55
+ if @input_params[:temp_device_name] == nil
56
+ @input_params[:temp_device_name] = "/dev/sdj"
57
+ end
58
+ if @input_params[:root_device_name] == nil
59
+ @input_params[:root_device_name] = "/dev/sda1"
60
+ end
61
+ # start state machine
62
+ current_state = Ami2EbsConversionState.load_state(@input_params)
63
+ @state_change_listeners.each() {|listener|
64
+ current_state.register_state_change_listener(listener)
65
+ }
66
+ end_state = current_state.start_state_machine()
67
+ if end_state.failed?
68
+ @result[:failed] = true
69
+ @result[:failure_reason] = current_state.end_state.failure_reason
70
+ @result[:end_state] = current_state.end_state
71
+ else
72
+ @result[:failed] = false
73
+ end
74
+ rescue Exception => e
75
+ @logger.warn "exception during encryption: #{e}"
76
+ @logger.warn e.backtrace.join("\n")
77
+ err = e.to_s
78
+ err += " (in #{current_state.end_state.to_s})" unless current_state == nil
79
+ @result[:failed] = true
80
+ @result[:failure_reason] = err
81
+ @result[:end_state] = current_state.end_state unless current_state == nil
82
+ ensure
83
+ begin
84
+ @input_params[:remote_command_handler].disconnect
85
+ rescue Exception => e2
86
+ end
87
+ end
88
+ #
89
+ @result[:done] = true
90
+ end
91
+
92
+ # Returns a hash with the following information:
93
+ # :done => if execution is done
94
+ #
95
+ def get_execution_result
96
+ @result
97
+ end
98
+
99
+ private
100
+
101
+ # Here begins the state machine implementation
102
+ class Ami2EbsConversionState < ScriptExecutionState
103
+ def self.load_state(context)
104
+ state = context[:initial_state] == nil ? InitialState.new(context) : context[:initial_state]
105
+ state
106
+ end
107
+
108
+ def connect
109
+ if @context[:remote_command_handler] == nil
110
+ @context[:remote_command_handler] = RemoteCommandHandler.new
111
+ end
112
+ connected = false
113
+ remaining_trials = 3
114
+ while !connected && remaining_trials > 0
115
+ remaining_trials -= 1
116
+ if @context[:ssh_keyfile] != nil
117
+ begin
118
+ @context[:remote_command_handler].connect_with_keyfile(@context[:dns_name], @context[:ssh_keyfile])
119
+ connected = true
120
+ rescue Exception => e
121
+ @logger.info("connection failed due to #{e}")
122
+ @logger.debug(e.backtrace.join("\n"))
123
+ end
124
+ elsif @context[:ssh_keydata] != nil
125
+ begin
126
+ @context[:remote_command_handler].connect(@context[:dns_name], "root", @context[:ssh_keydata])
127
+ connected = true
128
+ rescue Exception => e
129
+ @logger.info("connection failed due to #{e}")
130
+ @logger.debug(e.backtrace.join("\n"))
131
+ end
132
+ else
133
+ raise Exception.new("no key information specified")
134
+ end
135
+ if !connected
136
+ sleep(5) #try again
137
+ end
138
+ end
139
+ if !connected
140
+ raise Exception.new("connection attempts stopped")
141
+ end
142
+ @context[:result][:os] = @context[:remote_command_handler].retrieve_os()
143
+ @logger.info "connected to #{@context[:dns_name]}"
144
+ end
145
+
146
+ end
147
+
148
+ # Nothing done yet. Start by instantiating an AMI (in the right zone?)
149
+ # which serves to create
150
+ class InitialState < Ami2EbsConversionState
151
+ def enter
152
+ startup_ami()
153
+ end
154
+
155
+ private
156
+
157
+ def startup_ami()
158
+ @logger.debug "start up AMI #{@context[:ami_id]}"
159
+ res = @context[:ec2_api_handler].run_instances(:image_id => @context[:ami_id],
160
+ :security_group => @context[:security_group_name], :key_name => @context[:key_name])
161
+ instance_id = res['instancesSet']['item'][0]['instanceId']
162
+ @context[:instance_id] = instance_id
163
+ @logger.info "started instance #{instance_id}"
164
+ #availability_zone , key_name/group_name
165
+ started = false
166
+ while started == false
167
+ sleep(5)
168
+ res = @context[:ec2_api_handler].describe_instances(:instance_id => @context[:instance_id])
169
+ state = res['reservationSet']['item'][0]['instancesSet']['item'][0]['instanceState']
170
+ @logger.info "instance in state #{state['name']} (#{state['code']})"
171
+ if state['code'].to_i == 16
172
+ started = true
173
+ @context[:dns_name] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['dnsName']
174
+ @context[:availability_zone] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['placement']['availabilityZone']
175
+ @context[:kernel_id] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['kernelId']
176
+ @context[:ramdisk_id] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['ramdiskId']
177
+ @context[:architecture] = res['reservationSet']['item'][0]['instancesSet']['item'][0]['architecture']
178
+ elsif state['code'].to_i != 0
179
+ raise Exception.new('instance failed to start up')
180
+ end
181
+ end
182
+ AmiStarted.new(@context)
183
+ end
184
+ end
185
+
186
+ # Ami started. Create a storage
187
+ class AmiStarted < Ami2EbsConversionState
188
+ def enter
189
+ create_storage()
190
+ end
191
+
192
+ private
193
+
194
+ def create_storage()
195
+ @logger.debug "create volume in zone #{@context[:availability_zone]}"
196
+ res = @context[:ec2_api_handler].create_volume(:availability_zone => @context[:availability_zone], :size => "10")
197
+ @context[:volume_id] = res['volumeId']
198
+ started = false
199
+ while !started
200
+ sleep(5)
201
+ #TODO: check for timeout?
202
+ res = @context[:ec2_api_handler].describe_volumes(:volume_id => @context[:volume_id])
203
+ state = res['volumeSet']['item'][0]['status']
204
+ @logger.debug "volume state #{state}"
205
+ if state == 'available'
206
+ started = true
207
+ end
208
+ end
209
+ StorageCreated.new(@context)
210
+ end
211
+
212
+ end
213
+
214
+ # Storage created. Attach it.
215
+ class StorageCreated < Ami2EbsConversionState
216
+ def enter
217
+ attach_storage()
218
+ end
219
+
220
+ private
221
+
222
+ def attach_storage()
223
+ @logger.debug "attach volume #{@context[:volume_id]} to instance #{@context[:instance_id]} on device #{@context[:temp_device_name]}"
224
+ @context[:ec2_api_handler].attach_volume(:volume_id => @context[:volume_id],
225
+ :instance_id => @context[:instance_id],
226
+ :device => @context[:temp_device_name]
227
+ )
228
+ done = false
229
+ while !done
230
+ sleep(5)
231
+ #TODO: check for timeout?
232
+ res = @context[:ec2_api_handler].describe_volumes(:volume_id => @context[:volume_id])
233
+ state = res['volumeSet']['item'][0]['status']
234
+ @logger.debug "storage attaching: #{state}"
235
+ if state == 'in-use'
236
+ done = true
237
+ end
238
+ end
239
+ StorageAttached.new(@context)
240
+ end
241
+
242
+ end
243
+
244
+ # Storage attached. Create a file-system and moun it
245
+ class StorageAttached < Ami2EbsConversionState
246
+ def enter
247
+ create_fs()
248
+ end
249
+
250
+ private
251
+
252
+ def create_fs()
253
+ @logger.debug "create filesystem on #{@context[:dns_name]} to #{@context[:temp_device_name]}"
254
+ connect()
255
+ @context[:remote_command_handler].create_filesystem("ext3", @context[:temp_device_name])
256
+ FileSystemCreated.new(@context)
257
+ end
258
+ end
259
+
260
+ # File system created. Mount it.
261
+ class FileSystemCreated < Ami2EbsConversionState
262
+ def enter
263
+ mount_fs()
264
+ end
265
+
266
+ private
267
+
268
+ def mount_fs()
269
+ @context[:path] = "/mnt/tmp_#{@context[:volume_id]}"
270
+ @logger.debug "mount #{@context[:temp_device_name]} on #{@context[:path]}"
271
+ @context[:remote_command_handler].mkdir(@context[:path])
272
+ @context[:remote_command_handler].mount(@context[:temp_device_name], @context[:path])
273
+ sleep(2) #give mount some time
274
+ if !@context[:remote_command_handler].drive_mounted?(@context[:path])
275
+ raise Exception.new("drive #{@context[:path]} not mounted")
276
+ end
277
+ FileSystemMounted.new(@context)
278
+ end
279
+ end
280
+
281
+ # File system created and mounted. Copy the root partition.
282
+ class FileSystemMounted < Ami2EbsConversionState
283
+ def enter
284
+ copy()
285
+ end
286
+
287
+ private
288
+
289
+ def copy()
290
+ @logger.debug "start copying to #{@context[:path]}"
291
+ start = Time.new.to_i
292
+ @context[:remote_command_handler].rsync("/", "#{@context[:path]}", "/mnt/")
293
+ @context[:remote_command_handler].rsync("/dev/", "#{@context[:path]}/dev/")
294
+ endtime = Time.new.to_i
295
+ @logger.info "copy took #{(endtime-start)}s"
296
+ CopyDone.new(@context)
297
+ end
298
+ end
299
+
300
+ # Copy operation done. Unmount volume.
301
+ class CopyDone < Ami2EbsConversionState
302
+ def enter
303
+ unmount()
304
+ end
305
+
306
+ private
307
+
308
+ def unmount()
309
+ @logger.debug "unmount #{@context[:path]}"
310
+ @context[:remote_command_handler].umount(@context[:path])
311
+ sleep(2) #give umount some time
312
+ if @context[:remote_command_handler].drive_mounted?(@context[:path])
313
+ raise Exception.new("drive #{@context[:path]} not unmounted")
314
+ end
315
+ VolumeUnmounted.new(@context)
316
+ end
317
+ end
318
+
319
+ # Volume unmounted. Detach it.
320
+ class VolumeUnmounted < Ami2EbsConversionState
321
+ def enter
322
+ detach()
323
+ end
324
+
325
+ private
326
+
327
+ def detach()
328
+ @logger.debug "detach volume #{@context[:volume_id]}"
329
+ @context[:ec2_api_handler].detach_volume(:volume_id => @context[:volume_id],
330
+ :instance_id => @context[:instance_id]
331
+ )
332
+ done = false
333
+ while !done
334
+ sleep(3)
335
+ #TODO: check for timeout?
336
+ res = @context[:ec2_api_handler].describe_volumes(:volume_id => @context[:volume_id])
337
+ @logger.debug "volume detaching: #{res.inspect}"
338
+ if res['volumeSet']['item'][0]['status'] == 'available'
339
+ done = true
340
+ end
341
+ end
342
+ VolumeDetached.new(@context)
343
+ end
344
+ end
345
+
346
+
347
+ # VolumeDetached. Create snaphot
348
+ class VolumeDetached < Ami2EbsConversionState
349
+ def enter
350
+ create_snapshot()
351
+ end
352
+
353
+ private
354
+
355
+ def create_snapshot()
356
+ @logger.debug "create snapshot for volume #{@context[:volume_id]}"
357
+ res = @context[:ec2_api_handler].create_snapshot(:volume_id => @context[:volume_id])
358
+ @context[:snapshot_id] = res['snapshotId']
359
+ @logger.info "snapshot_id = #{@context[:snapshot_id]}"
360
+ done = false
361
+ while !done
362
+ sleep(5)
363
+ #TODO: check for timeout?
364
+ res = @context[:ec2_api_handler].describe_snapshots(:snapshot_id => @context[:snapshot_id])
365
+ @logger.debug "snapshot creating: #{res.inspect}"
366
+ if res['snapshotSet']['item'][0]['status'] == 'completed'
367
+ done = true
368
+ end
369
+ end
370
+ SnapshotCreated.new(@context)
371
+ end
372
+ end
373
+
374
+ # Snapshot created. Delete volume.
375
+ class SnapshotCreated < Ami2EbsConversionState
376
+ def enter
377
+ delete_volume()
378
+ end
379
+
380
+ private
381
+
382
+ def delete_volume
383
+ @logger.debug "delete volume #{@context[:volume_id]}"
384
+ res = @context[:ec2_api_handler].delete_volume(:volume_id => @context[:volume_id])
385
+ VolumeDeleted.new(@context)
386
+ end
387
+ end
388
+
389
+ # Volume deleted. Register snapshot.
390
+ class VolumeDeleted < Ami2EbsConversionState
391
+ def enter
392
+ register()
393
+ end
394
+
395
+ private
396
+
397
+ def register()
398
+ @logger.debug "register snapshot #{@context[:snapshot_id]} as #{@context[:name]}"
399
+ res = @context[:ec2_api_handler].register_image_updated(:snapshot_id => @context[:snapshot_id],
400
+ :kernel_id => @context[:kernel_id], :architecture => @context[:architecture],
401
+ :root_device_name => @context[:root_device_name],
402
+ :description => @context[:description], :name => @context[:name],
403
+ :ramdisk_id => @context[:ramdisk_id]
404
+ )
405
+ @logger.debug "result of registration = #{res.inspect}"
406
+ @context[:result][:image_id] = res['imageId']
407
+ @logger.info "resulting image_id = #{@context[:result][:image_id]}"
408
+ SnapshotRegistered.new(@context)
409
+ end
410
+ end
411
+
412
+ # Snapshot registered. Shutdown instance.
413
+ class SnapshotRegistered < Ami2EbsConversionState
414
+ def enter
415
+ shut_down()
416
+ end
417
+
418
+ private
419
+
420
+ def shut_down()
421
+ @logger.debug "shutdown instance #{@context[:instance_id]}"
422
+ res = @context[:ec2_api_handler].terminate_instances(:instance_id => @context[:instance_id])
423
+ done = false
424
+ while done == false
425
+ sleep(5)
426
+ res = @context[:ec2_api_handler].describe_instances(:instance_id => @context[:instance_id])
427
+ state = res['reservationSet']['item'][0]['instancesSet']['item'][0]['instanceState']
428
+ @logger.debug "instance in state #{state['name']} (#{state['code']})"
429
+ if state['code'].to_i == 48
430
+ done = true
431
+ elsif state['code'].to_i != 32
432
+ raise Exception.new('instance failed to shut down')
433
+ end
434
+ end
435
+ Done.new(@context)
436
+ end
437
+ end
438
+
439
+ # Instance shutdown. Done.
440
+ class Done < Ami2EbsConversionState
441
+ def done?
442
+ true
443
+ end
444
+ end
445
+
446
+ end