ec2-copy-snapshot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ .rvmrc
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "aws-sdk"
4
+ gem "net-ssh"
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Zach Wily
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Ec2::Copy::Snapshot
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'ec2-copy-snapshot'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install ec2-copy-snapshot
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require 'rubygems'
5
+ require 'optparse'
6
+ require 'aws-sdk'
7
+ require 'net/ssh'
8
+
9
+ opts = {
10
+ :aws_access_key => ENV["AWS_ACCESS_KEY_ID"],
11
+ :aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
12
+ :source_aws_region => 'us-east-1',
13
+ :destination_aws_region => nil,
14
+ :snapshot_id => nil,
15
+ :source_hostname => nil,
16
+ :destination_hostname => nil,
17
+ :keep_destination_volume => false,
18
+ }
19
+
20
+ parser = OptionParser.new do |o|
21
+ o.banner = "Usage: copy_snapshot_between_regions [options] snapshot-id"
22
+
23
+ o.on("--aws-access-key ACCESS_KEY", "AWS Access Key (default: AWS_ACCESS_KEY_ID)") do |v|
24
+ opts[:aws_access_key] = v
25
+ end
26
+
27
+ o.on("--aws-secret-access-key SECRET_KEY", "AWS Secret Access Key (default: AWS_SECRET_ACCESS_KEY)") do |v|
28
+ opts[:aws_secret_access_key] = v
29
+ end
30
+
31
+ o.on("--from-region REGION", "AWS Region to copy snapshot FROM (default: us-east-1)") do |v|
32
+ opts[:source_aws_region] = v
33
+ end
34
+
35
+ o.on("--to-region REGION", "AWS Region to copy snapshot TO") do |v|
36
+ opts[:destination_aws_region] = v
37
+ end
38
+
39
+ o.on("--from-instance HOSTNAME", "Hostname of an instance in the source region to use for the copy") do |v|
40
+ opts[:source_hostname] = v
41
+ end
42
+
43
+ o.on("--to-instance HOSTNAME", "Hostname of an instance in the target region to use for the copy") do |v|
44
+ opts[:destination_hostname] = v
45
+ end
46
+
47
+ o.on("--[no-]keep-volume", "Keep the destination volume the new snapshot is made from (default: no)") do |v|
48
+ opts[:keep_destination_volume] = v
49
+ end
50
+ end
51
+ parser.parse!
52
+
53
+ if ARGV.length == 1
54
+ opts[:snapshot_id] = ARGV.shift
55
+ end
56
+
57
+ if opts.values.any? {|v| v.nil? }
58
+ puts parser.help
59
+ exit 1
60
+ end
61
+
62
+ STDOUT.sync = true
63
+
64
+ ec2 = AWS::EC2.new(:access_key_id => opts[:aws_access_key], :secret_access_key => opts[:aws_secret_access_key])
65
+ ec2_source = ec2.regions[opts[:source_aws_region]]
66
+ ec2_destination = ec2.regions[opts[:destination_aws_region]]
67
+
68
+ def wait_for(interval = 5, &block)
69
+ while true
70
+ print "."
71
+ sleep interval
72
+ break if yield
73
+ end
74
+ end
75
+
76
+ def available_dev(instance)
77
+ # Returns a device that appears to be unused. There is a race condition here
78
+ # if several people are using this script with the same instances at the same
79
+ # time, but worst case the script will fail when we try to attach to a device
80
+ # that somebody else has started using.
81
+ device_mappings = instance.block_device_mappings
82
+ possible_devs = ('f'..'z').to_a.map {|l| "/dev/sd#{l}" } - device_mappings.keys
83
+ return nil if possible_devs.empty?
84
+ possible_devs.sample
85
+ end
86
+
87
+ def wait_for_volume(volume, desc)
88
+ print "Waiting for #{desc} volume #{volume.id}..."
89
+ wait_for { volume.status != :creating }
90
+ puts "done."
91
+
92
+ if volume.status != :available
93
+ raise "Volume is not available (#{volume.status})"
94
+ end
95
+ end
96
+
97
+ def attach_volume_to_instance(volume, instance, desc)
98
+ dev = available_dev(instance)
99
+ if dev.nil?
100
+ raise "Can't find a dev on #{instance.id}"
101
+ end
102
+
103
+ puts "Attaching #{desc} volume to instance #{instance.id} at #{dev}..."
104
+ attachment = volume.attach_to(instance, dev)
105
+ return attachment
106
+ end
107
+
108
+ def wait_for_attachment(attachment, desc)
109
+ print "Waiting for #{desc} volume to be attached..."
110
+ wait_for { attachment.status != :attaching }
111
+ puts "done."
112
+
113
+ if attachment.status != :attached
114
+ raise "Volume did not successfully attach (#{attachment.status})"
115
+ end
116
+ end
117
+
118
+ def detach_attachment(attachment, desc)
119
+ puts "Detaching #{desc} #{attachment.volume.id}"
120
+ attachment.delete(:force => true)
121
+ end
122
+
123
+ def wait_for_detachment(attachment, desc)
124
+ print "Waiting for #{desc} volume to be detached..."
125
+ volume = attachment.volume
126
+ wait_for { volume.status != :in_use }
127
+ puts "done."
128
+
129
+ if attachment.volume.status != :available
130
+ raise "Volume did not successfully detach #{attachment.volume.status})"
131
+ end
132
+ end
133
+
134
+ def delete_volume(volume, desc)
135
+ puts "Deleting #{desc} #{volume.id}..."
136
+ volume.delete
137
+ end
138
+
139
+ def mangled_dev(dev)
140
+ # Newer versions of Linux address the sdX volumes as xvdX
141
+ dev.gsub('/dev/sd', '/dev/xvd')
142
+ end
143
+
144
+ begin
145
+ print "Logging into #{opts[:source_hostname]}..."
146
+ source_ssh = Net::SSH.start(opts[:source_hostname], nil)
147
+ source_instance_id = source_ssh.exec!("curl -s http://169.254.169.254/latest/meta-data/instance-id")
148
+ source_instance = ec2_source.instances[source_instance_id]
149
+ puts " #{source_instance_id}"
150
+
151
+ print "Logging into #{opts[:destination_hostname]}..."
152
+ destination_ssh = Net::SSH.start(opts[:destination_hostname], nil)
153
+ destination_instance_id = destination_ssh.exec!("curl -s http://169.254.169.254/latest/meta-data/instance-id")
154
+ destination_instance = ec2_destination.instances[destination_instance_id]
155
+ puts " #{destination_instance_id}"
156
+
157
+ puts "Getting snapshot information..."
158
+ source_snapshot = ec2_source.snapshots[opts[:snapshot_id]]
159
+ if source_snapshot.status != :completed
160
+ raise "Snapshot does not appear complete (#{source_snapshot.status})"
161
+ end
162
+
163
+ print "Creating a volume from the snapshot..."
164
+ source_volume = source_snapshot.create_volume(source_instance.availability_zone)
165
+ puts " #{source_volume.id}"
166
+
167
+ print "Creating a destination volume..."
168
+ destination_volume = ec2_destination.volumes.create(
169
+ :size => source_volume.size,
170
+ :availability_zone => destination_instance.availability_zone)
171
+ puts " #{destination_volume.id}"
172
+
173
+ wait_for_volume(source_volume, "source")
174
+ wait_for_volume(destination_volume, "destination")
175
+
176
+ source_attachment = attach_volume_to_instance(source_volume, source_instance, "source")
177
+ destination_attachment = attach_volume_to_instance(destination_volume, destination_instance, "destination")
178
+
179
+ wait_for_attachment(source_attachment, "source")
180
+ wait_for_attachment(destination_attachment, "destination")
181
+
182
+ puts "Copying data from source to destination, this may take awhile..."
183
+ source_ssh.exec!(%Q{
184
+ catcmd="cat"; if [[ $(which pv) ]]; then catcmd="pv -f"; fi; \
185
+ sudo $catcmd #{mangled_dev(source_attachment.device)} | dd bs=1M | bzip2 --compress | \
186
+ ssh root@#{opts[:destination_hostname]} "bzip2 --decompress | dd of=#{mangled_dev(destination_attachment.device)} bs=1M"
187
+ }) do |channel, stream, data|
188
+ STDOUT.print data if stream == :stdout
189
+ STDERR.print data if stream == :stderr
190
+ end
191
+
192
+ print "Creating snapshot of new volume..."
193
+ destination_snapshot = destination_volume.create_snapshot("created by #{`whoami`} of #{opts[:snapshot_id]} from #{opts[:source_aws_region]}")
194
+ puts " #{destination_snapshot.id}"
195
+
196
+ print "Waiting for snapshot to complete..."
197
+ wait_for { destination_snapshot.status != :pending }
198
+ puts "done."
199
+
200
+ if destination_snapshot.status != :completed
201
+ raise "Could not create snapshot (#{destination_snapshot.status})"
202
+ end
203
+ ensure
204
+ puts "Cleaning up..."
205
+
206
+ # Start detaching the source and dest volumes in parallel, cause they might take awhile
207
+ detach_attachment(source_attachment, "source") if source_attachment
208
+ detach_attachment(destination_attachment, "destination") if destination_attachment && !opts[:keep_destination_volume]
209
+
210
+ wait_for_detachment(source_attachment, "source") if source_attachment
211
+ wait_for_detachment(destination_attachment, "destination") if destination_attachment && !opts[:keep_destination_volume]
212
+
213
+ delete_volume(source_volume, "source") if source_volume
214
+ delete_volume(destination_volume, "destination") if destination_volume && !opts[:keep_destination_volume]
215
+
216
+ puts "Closing SSH connections..."
217
+ destination_ssh.close if destination_ssh
218
+ source_ssh.close if source_ssh
219
+ end
220
+
221
+ puts "New snapshot created: #{destination_snapshot.id}" if destination_snapshot
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "ec2-copy-snapshot"
7
+ gem.version = "0.1.0"
8
+ gem.authors = ["Zach Wily"]
9
+ gem.email = ["zach@zwily.com"]
10
+ gem.description = %q{Script to copy an EC2 snapshot between regions}
11
+ gem.summary = %q{Script to copy an EC2 snapshot between regions}
12
+ gem.homepage = "https://github.com/zwily/ec2-copy-snapshot"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency('aws-sdk')
20
+ gem.add_dependency('net-ssh')
21
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec2-copy-snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Zach Wily
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: net-ssh
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Script to copy an EC2 snapshot between regions
47
+ email:
48
+ - zach@zwily.com
49
+ executables:
50
+ - ec2-copy-snapshot
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - bin/ec2-copy-snapshot
60
+ - ec2-copy-snapshot.gemspec
61
+ homepage: https://github.com/zwily/ec2-copy-snapshot
62
+ licenses: []
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.24
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Script to copy an EC2 snapshot between regions
85
+ test_files: []
86
+ has_rdoc: