CloudyScripts 0.0.14 → 1.4.15

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ require 'rake/testtask'
12
12
 
13
13
  spec = Gem::Specification.new do |s|
14
14
  s.name = 'CloudyScripts'
15
- s.version = '0.0.14'
15
+ s.version = '1.4.15'
16
16
  s.has_rdoc = true
17
17
  s.extra_rdoc_files = ['README.rdoc', 'LICENSE']
18
18
  s.summary = 'Scripts to facilitate programming for infrastructure clouds.'
@@ -59,4 +59,41 @@ class Ec2Helper
59
59
  end
60
60
  return vols['volumeSet']['item'][0][prop.to_s]
61
61
  end
62
+
63
+ def snapshot_prop(snapshot_id, prop)
64
+ snaps = @ec2_api.describe_snapshots(:snapshot_id => snapshot_id)
65
+ begin
66
+ if snaps['snapshotSet']['item'].size == 0
67
+ raise Exception.new("snapshot #{snapshot_id} not found")
68
+ end
69
+ return snaps['snapshotSet']['item'][0][prop.to_s]
70
+ rescue
71
+ raise Exception.new("snapshot #{snapshot_id} not found")
72
+ end
73
+ end
74
+
75
+ def ami_prop(ami_id, prop)
76
+ amis = @ec2_api.describe_images(:image_id => ami_id)
77
+ begin
78
+ if amis['imagesSet']['item'].size == 0
79
+ raise Exception.new("image #{ami_id} not found")
80
+ end
81
+ return amis['imagesSet']['item'][0][prop.to_s]
82
+ rescue
83
+ raise Exception.new("image #{ami_id} not found")
84
+ end
85
+ end
86
+
87
+ def instance_prop(instance_id, prop)
88
+ instances = @ec2_api.describe_instances(:instance_id => instance_id)
89
+ begin
90
+ if instances['reservationSet']['item'][0]['instancesSet']['item'].size == 0
91
+ raise Exception.new("instance #{instance_id} not found")
92
+ end
93
+ return instances['reservationSet']['item'][0]['instancesSet']['item'][prop.to_s]
94
+ rescue
95
+ raise Exception.new("instance #{instance_id} not found")
96
+ end
97
+ end
98
+
62
99
  end
@@ -1,5 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'net/ssh'
3
+ require 'net/scp'
4
+ require 'timeout'
3
5
 
4
6
  # Provides methods to be executed via ssh to remote instances.
5
7
  class RemoteCommandHandler
@@ -102,14 +104,21 @@ class RemoteCommandHandler
102
104
  end
103
105
 
104
106
  # Copy directory using options -avHx
105
- def rsync(source_path, dest_path, exclude_path = nil)
107
+ def local_rsync(source_path, dest_path, exclude_path = nil)
106
108
  exclude = ""
107
109
  if exclude_path != nil
108
110
  exclude = "--exclude #{exclude_path}"
109
111
  end
110
112
  e = "rsync -avHx #{exclude} #{source_path} #{dest_path}"
111
113
  @logger.debug "going to execute #{e}"
112
- remote_exec_helper(e, nil, nil, false)
114
+ remote_exec_helper(e, nil, nil, false) #TODO: handle output in stderr?
115
+ end
116
+
117
+ # Copy directory via an ssh-tunnel.
118
+ def remote_rsync(keyfile, source_path, dest_ip, dest_path)
119
+ e = "rsync -rlpgoDzq -e "+'"'+"ssh -o stricthostkeychecking=no -i #{keyfile}"+'"'+" #{source_path} root@#{dest_ip}:#{dest_path}"
120
+ @logger.debug "going to execute #{e}"
121
+ remote_exec_helper(e, nil, nil, false) #TODO: handle output in stderr?
113
122
  end
114
123
 
115
124
  # Zip the complete contents of the source path into the destination file.
@@ -122,6 +131,15 @@ class RemoteCommandHandler
122
131
  end
123
132
  end
124
133
 
134
+ def echo(data, file)
135
+ exec = "echo #{data} > #{file}"
136
+ @logger.debug "going to execute #{exec}"
137
+ remote_execute(exec, nil, true)
138
+ if !file_exists?(file)
139
+ raise Exception.new("file #{file} could not be created")
140
+ end
141
+ end
142
+
125
143
  # Executes the specified #exec_string on a remote session specified.
126
144
  # When #push_data is specified, the data will be used as input for the
127
145
  # command and thus allow to respond in advance to commands that ask the user
@@ -168,6 +186,14 @@ class RemoteCommandHandler
168
186
  stdout.join()
169
187
  end
170
188
 
189
+ def upload(ip, user, key_data, local_file, destination_file, timeout = 60)
190
+ Timeout::timeout(timeout) {
191
+ Net::SCP.start(ip, user, {:key_data => [key_data], :timeout => timeout}) do |scp|
192
+ scp.upload!(local_file, destination_file)
193
+ end
194
+ }
195
+ end
196
+
171
197
  private
172
198
 
173
199
  # Executes the specified #exec_string on the opened remote session.
@@ -1,6 +1,8 @@
1
+ require 'net/scp'
2
+
1
3
  # Contains methods that are used by the scripts in the state-machines. Since
2
4
  # they are reused by different scripts, they are factored into this module.
3
- #
5
+ #
4
6
  # Note: it is supposed that a hash named @context exists @context[:script]
5
7
  # must be set to a script object to pass information and messages
6
8
  # to listeners.
@@ -20,34 +22,39 @@ module StateTransitionHelper
20
22
  def connect(dns_name, ssh_keyfile = nil, ssh_keydata = nil)
21
23
  post_message("connecting to #{dns_name}...")
22
24
  connected = false
23
- remaining_trials = 3
25
+ last_connection_problem = ""
26
+ remaining_trials = 5
24
27
  while !connected && remaining_trials > 0
25
28
  remaining_trials -= 1
26
29
  if ssh_keyfile != nil
27
30
  begin
31
+ @logger.info("connecting using keyfile")
28
32
  remote_handler().connect_with_keyfile(dns_name, ssh_keyfile)
29
33
  connected = true
30
34
  rescue Exception => e
31
35
  @logger.info("connection failed due to #{e}")
32
- @logger.debug(e.backtrace.join("\n"))
36
+ last_connection_problem = "#{e}"
37
+ @logger.debug(e.backtrace.select(){|line| line.include?("state_transition_helper")}.join("\n"))
33
38
  end
34
39
  elsif ssh_keydata != nil
35
40
  begin
41
+ @logger.info("connecting using keydata")
36
42
  remote_handler().connect(dns_name, "root", ssh_keydata)
37
43
  connected = true
38
44
  rescue Exception => e
39
45
  @logger.info("connection failed due to #{e}")
40
- @logger.debug(e.backtrace.join("\n"))
46
+ last_connection_problem = "#{e}"
47
+ @logger.debug(e.backtrace.select(){|line| line.include?("state_transition_helper")}.join("\n"))
41
48
  end
42
49
  else
43
50
  raise Exception.new("no key information specified")
44
51
  end
45
52
  if !connected
46
- sleep(5) #try again
53
+ sleep(20) #try again
47
54
  end
48
55
  end
49
56
  if !connected
50
- raise Exception.new("connection attempts stopped")
57
+ raise Exception.new("connection attempts stopped (#{last_connection_problem})")
51
58
  end
52
59
  os = remote_handler().retrieve_os()
53
60
  post_message("connected to #{dns_name}. OS installed is #{os}")
@@ -55,6 +62,15 @@ module StateTransitionHelper
55
62
  return os
56
63
  end
57
64
 
65
+ # If a remote command handler is connected, disconnect him silently.
66
+ def disconnect
67
+ begin
68
+ remote_handler().disconnect()
69
+ rescue
70
+ end
71
+ self.remote_handler= nil
72
+ end
73
+
58
74
  # Launch an instance based on an AMI ID
59
75
  # Input Parameters:
60
76
  # * ami_id => ID of the AMI to be launched
@@ -67,11 +83,12 @@ module StateTransitionHelper
67
83
  # * kernel_id => EC2 Kernel ID of the started instance
68
84
  # * ramdisk_id => EC2 Ramdisk ID of the started instance
69
85
  # * architecture => architecture (e.g. 386i, 64x) of the started instance
70
- def launch_instance(ami_id, key_name, security_group_name)
86
+ def launch_instance(ami_id, key_name, security_group_name, ec2_handler = nil)
87
+ ec2_handler = ec2_handler() if ec2_handler == nil
71
88
  post_message("starting up instance to execute the script (AMI = #{ami_id}) ...")
72
89
  @logger.debug "start up AMI #{ami_id}"
73
90
  # find out the image architecture first
74
- image_props = ec2_handler().describe_images(:image_id => ami_id)
91
+ image_props = ec2_handler.describe_images(:image_id => ami_id)
75
92
  architecture = image_props['imagesSet']['item'][0]['architecture']
76
93
  instance_type = "m1.small"
77
94
  if architecture != "i386"
@@ -81,7 +98,7 @@ module StateTransitionHelper
81
98
  @logger.info arch_log_msg
82
99
  post_message(arch_log_msg)
83
100
  # now start it
84
- res = ec2_handler().run_instances(:image_id => ami_id,
101
+ res = ec2_handler.run_instances(:image_id => ami_id,
85
102
  :security_group => security_group_name, :key_name => key_name,
86
103
  :instance_type => instance_type
87
104
  )
@@ -92,7 +109,7 @@ module StateTransitionHelper
92
109
  started = false
93
110
  while started == false
94
111
  sleep(5)
95
- res = ec2_handler().describe_instances(:instance_id => instance_id)
112
+ res = ec2_handler.describe_instances(:instance_id => instance_id)
96
113
  state = res['reservationSet']['item'][0]['instancesSet']['item'][0]['instanceState']
97
114
  @logger.info "instance is in state #{state['name']} (#{state['code']})"
98
115
  if state['code'].to_i == 16
@@ -252,10 +269,11 @@ module StateTransitionHelper
252
269
  # * volume_id => EC2 ID for the EBS volume to be snapshotted
253
270
  # Returns:
254
271
  # * snapshot_id => EC2 ID for the snapshot created
255
- def create_snapshot(volume_id)
272
+ def create_snapshot(volume_id, description = "")
256
273
  post_message("going to create a snapshot for volume #{volume_id}...")
257
274
  @logger.debug "create snapshot for volume #{volume_id}"
258
- res = ec2_handler().create_snapshot(:volume_id => volume_id)
275
+ res = ec2_handler().create_snapshot(:volume_id => volume_id,
276
+ :description => description)
259
277
  snapshot_id = res['snapshotId']
260
278
  @logger.info "snapshot_id = #{snapshot_id}"
261
279
  done = false
@@ -358,8 +376,8 @@ module StateTransitionHelper
358
376
  post_message("going to start copying files to #{destination_path}. This may take quite a time...")
359
377
  @logger.debug "start copying to #{destination_path}"
360
378
  start = Time.new.to_i
361
- remote_handler().rsync("/", "#{destination_path}", "#{destination_path}")
362
- remote_handler().rsync("/dev/", "#{destination_path}/dev/")
379
+ remote_handler().local_rsync("/", "#{destination_path}", "#{destination_path}")
380
+ remote_handler().local_rsync("/dev/", "#{destination_path}/dev/")
363
381
  endtime = Time.new.to_i
364
382
  @logger.info "copy took #{(endtime-start)}s"
365
383
  post_message("copying is done (took #{endtime-start})s")
@@ -376,23 +394,52 @@ module StateTransitionHelper
376
394
  post_message("EBS volume successfully zipped")
377
395
  end
378
396
 
379
- protected
397
+ def remote_copy(keyname, source_dir, dest_machine, dest_dir)
398
+ post_message("going to remote copy all files from volume. This may take some time...")
399
+ remote_handler().remote_rsync("/root/.ssh/#{keyname}.pem", source_dir, dest_machine, dest_dir)
400
+ post_message("remote copy operation done")
401
+ end
380
402
 
381
- def post_message(msg)
382
- if @context[:script] != nil
383
- @context[:script].post_message(msg)
384
- end
403
+ def upload_file(ip, user, key_data, file, target_file)
404
+ post_message("going to upload #{file} to #{ip}:/#{target_file}")
405
+ remote_handler().upload(ip, user, key_data, file, target_file)
385
406
  end
386
407
 
408
+ #setting/retrieving handlers
409
+
387
410
  def remote_handler()
388
- if @context[:remote_command_handler] == nil
389
- @context[:remote_command_handler] = RemoteCommandHandler.new
411
+ if @remote_handler == nil
412
+ if @context[:remote_command_handler] == nil
413
+ @context[:remote_command_handler] = RemoteCommandHandler.new
414
+ else
415
+ @remote_handler = @context[:remote_command_handler]
416
+ end
390
417
  end
391
- @context[:remote_command_handler]
418
+ @remote_handler
419
+ end
420
+
421
+ def remote_handler=(remote_handler)
422
+ @remote_handler = remote_handler
392
423
  end
393
424
 
394
425
  def ec2_handler()
395
- @context[:ec2_api_handler]
426
+ if @ec2_handler == nil
427
+ @ec2_handler = @context[:ec2_api_handler]
428
+ end
429
+ @ec2_handler
430
+ end
431
+
432
+ def ec2_handler=(ec2_handler)
433
+ @ec2_handler = ec2_handler
434
+ end
435
+
436
+
437
+ protected
438
+
439
+ def post_message(msg)
440
+ if @context[:script] != nil
441
+ @context[:script].post_message(msg)
442
+ end
396
443
  end
397
444
 
398
445
  end
@@ -0,0 +1,188 @@
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 "help/ec2_helper"
6
+ require "AWS"
7
+
8
+ # Copy a given snapshot to another region
9
+ # * start up instance in source-region, create volume from snapshot, attach volume, and mount it
10
+ # * start up instance in destination-region, create empty volume of same size, attache volume, and mount it
11
+ # * copy the destination key to the source instance
12
+ # * perform an rsynch
13
+ # sync -PHAXaz --rsh "ssh -i /home/${src_user}/.ssh/id_${dst_keypair}" --rsync-path "sudo rsync" ${src_dir}/ ${dst_user}@${dst_public_fqdn}:${dst_dir}/
14
+ # * create a snapshot of the volume
15
+ # * clean-up everything
16
+
17
+ class CopySnapshot< Ec2Script
18
+ # context information needed
19
+ # * the EC2 credentials (see #Ec2Script)
20
+ # * snapshot_id => The ID of the snapshot to be downloaded
21
+ # * target_ec2_handler => The EC2 handler connected to the region where the snapshot is being copied to
22
+ # * source_key_name => Key name of the instance that manages the snaphot-volume in the source region
23
+ # * source_ssh_key_data => Key information for the security group that starts the AMI [if not set, use ssh_key_files]
24
+ # * source_ssh_key_files => Key information for the security group that starts the AMI
25
+ # * target_key_name => Key name of the instance that manages the snaphot-volume in the target region
26
+ # * target_ssh_key_data => Key information for the security group that starts the AMI [if not set, use ssh_key_files]
27
+ # * target_ssh_key_files => Key information for the security group that starts the AMI
28
+ # * source_ami_id => ID of the AMI to start in the source region
29
+ # * target_ami_id => ID of the AMI to start in the target region
30
+
31
+ def initialize(input_params)
32
+ super(input_params)
33
+ end
34
+
35
+ def check_input_parameters()
36
+ end
37
+
38
+ # Load the initial state for the script.
39
+ # Abstract method to be implemented by extending classes.
40
+ def load_initial_state()
41
+ CopySnapshotState.load_state(@input_params)
42
+ end
43
+
44
+ private
45
+
46
+ # Here begins the state machine implementation
47
+ class CopySnapshotState < ScriptExecutionState
48
+
49
+ def self.load_state(context)
50
+ InitialState.new(context)
51
+ end
52
+
53
+ def local_region
54
+ self.ec2_handler=(@context[:ec2_api_handler])
55
+ end
56
+
57
+ def remote_region
58
+ self.ec2_handler=(@context[:target_ec2_handler])
59
+ end
60
+ end
61
+
62
+ # Initial state: start up AMI in source region
63
+ class InitialState < CopySnapshotState
64
+ def enter()
65
+ result = launch_instance(@context[:source_ami_id], @context[:source_key_name], "default")
66
+ @context[:source_instance_id] = result.first
67
+ @context[:source_dns_name] = result[1]
68
+ @context[:source_availability_zone] = result[2]
69
+ SourceInstanceLaunchedState.new(@context)
70
+ end
71
+ end
72
+
73
+ # Source is started. Create a volume from the snapshot, attach and mount the volume
74
+ class SourceInstanceLaunchedState < CopySnapshotState
75
+ def enter()
76
+ @context[:source_volume_id] = create_volume_from_snapshot(@context[:snapshot_id],
77
+ @context[:source_availability_zone])
78
+ device = "/dev/sdj" #TODO: make device configurable
79
+ mount_point = "/mnt/tmp_#{@context[:source_volume_id]}"
80
+ attach_volume(@context[:source_volume_id], @context[:source_instance_id], device)
81
+ connect(@context[:source_dns_name], nil, @context[:source_ssh_keydata])
82
+ mount_fs(mount_point, device)
83
+ disconnect()
84
+ SourceVolumeReadyState.new(@context)
85
+ end
86
+ end
87
+
88
+ # Source is ready. Now start instance in the target region
89
+ class SourceVolumeReadyState < CopySnapshotState
90
+ def enter()
91
+ remote_region()
92
+ result = launch_instance(@context[:target_ami_id], @context[:target_key_name],
93
+ "default")
94
+ @context[:target_instance_id] = result.first
95
+ @context[:target_dns_name] = result[1]
96
+ @context[:target_availability_zone] = result[2]
97
+ TargetInstanceLaunchedState.new(@context)
98
+ end
99
+ end
100
+
101
+ # Destination instance is started. Now configure storage.
102
+ class TargetInstanceLaunchedState < CopySnapshotState
103
+ def enter()
104
+ local_region()
105
+ ec2_helper = Ec2Helper.new(@context[:ec2_api_handler])
106
+ volume_size = ec2_helper.snapshot_prop(@context[:snapshot_id], :volumeSize).to_i
107
+ #
108
+ remote_region()
109
+ @context[:target_volume_id] = create_volume(@context[:target_availability_zone], volume_size)
110
+ device = "/dev/sdj" #TODO: make device configurable
111
+ mount_point = "/mnt/tmp_#{@context[:target_volume_id]}"
112
+ attach_volume(@context[:target_volume_id], @context[:target_instance_id], device)
113
+ connect(@context[:target_dns_name], nil, @context[:target_ssh_keydata])
114
+ create_fs(@context[:target_dns_name], device)
115
+ mount_fs(mount_point, device)
116
+ disconnect()
117
+ TargetVolumeReadyState.new(@context)
118
+ end
119
+ end
120
+
121
+ # Storages are ready. Only thing missing: the key of the target region
122
+ # must be available on the instance in the source region to be able to perform
123
+ # a remote copy.
124
+ class TargetVolumeReadyState < CopySnapshotState
125
+ def enter()
126
+ post_message("upload key of target-instance to source-instance...")
127
+ upload_file(@context[:source_dns_name], "root", @context[:source_ssh_keydata],
128
+ @context[:target_ssh_keyfile], "/root/.ssh/#{@context[:target_key_name]}.pem")
129
+ post_message("credentials are in place to connect source and target.")
130
+ KeyInPlaceState.new(@context)
131
+ end
132
+ end
133
+
134
+ # Now we can copy.
135
+ class KeyInPlaceState < CopySnapshotState
136
+ def enter()
137
+ connect(@context[:source_dns_name], nil, @context[:source_ssh_keydata])
138
+ source_dir = "/mnt/tmp_#{@context[:source_volume_id]}/"
139
+ dest_dir = "/mnt/tmp_#{@context[:target_volume_id]}"
140
+ remote_copy(@context[:target_key_name], source_dir, @context[:target_dns_name], dest_dir)
141
+ disconnect()
142
+ DataCopiedState.new(@context)
143
+ end
144
+ end
145
+
146
+ # Data of snapshot now copied to the new volume. Create a snapshot of the
147
+ # new volume.
148
+ class DataCopiedState < CopySnapshotState
149
+ def enter()
150
+ remote_region()
151
+ @context[:new_snapshot_id] = create_snapshot(@context[:target_volume_id], "Created by Cloudy_Scripts - copy_snapshot")
152
+ @context[:result][:snapshot_id] = @context[:new_snapshot_id]
153
+ SnapshotCreatedState.new(@context)
154
+ end
155
+ end
156
+
157
+ # Operation done. Now only cleanup is missing, i.e. shut down instances and
158
+ # remote the volumes that were created. Start with cleaning the ressources
159
+ # in the local region.
160
+ class SnapshotCreatedState < CopySnapshotState
161
+ def enter()
162
+ local_region()
163
+ shut_down_instance(@context[:source_instance_id])
164
+ delete_volume(@context[:source_volume_id])
165
+ SourceCleanedUpState.new(@context)
166
+ end
167
+ end
168
+
169
+ # Cleanup the resources in the target region.
170
+ class SourceCleanedUpState < CopySnapshotState
171
+ def enter()
172
+ remote_region()
173
+ shut_down_instance(@context[:target_instance_id])
174
+ delete_volume(@context[:target_volume_id])
175
+ Done.new(@context)
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+ #Cloudy_Script: copy snapshots between regions
182
+ #start up instance in source-region, create volume from snapshot, attach volume, and mount it
183
+ #start up instance in destination-region, create empty volume of same size, attache volume, and mount it
184
+ #copy the destination key to the source instance
185
+ #perform an rsynch
186
+ #sync -PHAXaz --rsh "ssh -i /home/${src_user}/.ssh/id_${dst_keypair}" --rsync-path "sudo rsync" ${src_dir}/ ${dst_user}@${dst_public_fqdn}:${dst_dir}/
187
+ #create a snapshot of the volume
188
+ #clean-up everything
@@ -21,6 +21,7 @@ class DownloadSnapshot < Ec2Script
21
21
  # * the EC2 credentials (see #Ec2Script)
22
22
  # * ami_id: the ID of the AMI to be started to perform the operations and to run the web-server for download
23
23
  # * security_group_name => name of the security group used to start the AMI (should open ports for SSH and HTTP)
24
+ # * key_name => Name of the key to be used to access the instance providing the download
24
25
  # * ssh_key_data => Key information for the security group that starts the AMI [if not set, use ssh_key_files]
25
26
  # * ssh_key_files => Key information for the security group that starts the AMI
26
27
  # * snapshot_id => The ID of the snapshot to be downloaded
metadata CHANGED
@@ -3,10 +3,10 @@ name: CloudyScripts
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
- - 0
7
- - 0
8
- - 14
9
- version: 0.0.14
6
+ - 1
7
+ - 4
8
+ - 15
9
+ version: 1.4.15
10
10
  platform: ruby
11
11
  authors:
12
12
  - Matthias Jung
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-03-25 00:00:00 +01:00
17
+ date: 2010-04-27 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -63,6 +63,7 @@ files:
63
63
  - lib/help/state_change_listener.rb
64
64
  - lib/help/state_transition_helper.rb
65
65
  - lib/scripts/ec2/ami2_ebs_conversion.rb
66
+ - lib/scripts/ec2/copy_snapshot.rb
66
67
  - lib/scripts/ec2/dm_encrypt.rb
67
68
  - lib/scripts/ec2/download_snapshot.rb
68
69
  - lib/scripts/ec2/ec2_script.rb