CloudyScripts 0.0.14 → 1.4.15

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.
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