CloudyScripts 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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