rudy 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rudy.rb CHANGED
@@ -1,29 +1,57 @@
1
1
 
2
- require 'right_aws'
3
- require 'stringio'
4
- require 'ostruct'
5
- require 'yaml'
2
+ #
3
+ # No Ruby 1.9.1 support. Only 1.8.x for now :[
4
+ unless RUBY_VERSION < "1.9"
5
+ puts "Sorry! We're using the right_aws gem and it doesn't support Ruby 1.9 (md5 error)."
6
+ exit 1
7
+ end
8
+
9
+
10
+ begin
11
+ require 'digest/md5'
12
+ require 'right_aws'
13
+ require 'stringio'
14
+ require 'ostruct'
15
+ require 'yaml'
16
+ require 'socket'
17
+ require 'tempfile'
18
+
19
+ require 'console'
20
+ require 'storable'
21
+
22
+ require 'net/ssh'
23
+ require 'net/ssh/gateway'
24
+ require 'net/ssh/multi'
25
+ require 'net/scp'
26
+
27
+ rescue LoadError => ex
28
+ puts "Problem requiring: #{ex.message}"
29
+ exit 1
30
+ end
31
+
6
32
 
7
- require 'console'
8
- require 'storable'
9
33
 
10
34
  module Rudy #:nodoc:
11
- RUDY_DOMAIN = ENV['RUDY_DOMAIN'] || "rudy_state"
12
- RUDY_DELIM = ENV['RUDY_DELIM'] || '-'
13
- RUDY_CONFIG = File.join(ENV['HOME'] || ENV['USERPROFILE'], '.rudy')
35
+ RUDY_DOMAIN = "rudy_state"
36
+ RUDY_DELIM = '-'
37
+
38
+ RUDY_CONFIG_DIR = File.join(ENV['HOME'] || ENV['USERPROFILE'], '.rudy')
39
+ RUDY_CONFIG_FILE = File.join(RUDY_CONFIG_DIR, 'config')
14
40
 
15
- DEFAULT_REGION = ENV['EC2_DEFAULT_REGION'] || 'us-east-1'
16
- DEFAULT_ZONE = ENV['EC2_DEFAULT_ZONE'] || 'us-east-1b'
17
- DEFAULT_ENVIRONMENT = ENV['EC2_DEFAULT_ENVIRONMENT'] || 'stage'
18
- DEFAULT_ROLE = ENV['EC2_DEFAULT_ROLE'] || 'app'
19
- DEFAULT_POSITION = ENV['EC2_DEFAULT_POSITION'] || '01'
41
+ DEFAULT_REGION = 'us-east-1'
42
+ DEFAULT_ZONE = 'us-east-1b'
43
+ DEFAULT_ENVIRONMENT = 'stage'
44
+ DEFAULT_ROLE = 'app'
45
+ DEFAULT_POSITION = '01'
20
46
 
21
- DEFAULT_USER = ENV['EC2_DEFAULT_USER'] || 'rudy'
47
+ DEFAULT_USER = 'rudy'
48
+
49
+ SUPPORTED_SCM_NAMES = [:svn, :git]
22
50
 
23
51
  module VERSION #:nodoc:
24
52
  MAJOR = 0.freeze unless defined? MAJOR
25
- MINOR = 3.freeze unless defined? MINOR
26
- TINY = 2.freeze unless defined? TINY
53
+ MINOR = 4.freeze unless defined? MINOR
54
+ TINY = 0.freeze unless defined? TINY
27
55
  def self.to_s
28
56
  [MAJOR, MINOR, TINY].join('.')
29
57
  end
@@ -41,43 +69,18 @@ module Rudy #:nodoc:
41
69
  def self.in_situ?
42
70
  File.exists?('/etc/ec2/instance-id')
43
71
  end
44
-
45
- class Config < Storable
46
-
47
- attr_accessor :path
48
-
49
- field :userdata => Hash
50
-
51
- def initialize(args={:path => nil})
52
- @path = args[:path] || RUDY_CONFIG
53
- @userdata = {
54
- 'default' => {
55
- }
56
- }
57
- end
58
-
59
- def get(name)
60
-
61
- end
62
-
63
- def exists?
64
- File.exists?(@path)
65
- end
66
- end
67
72
  end
68
73
 
69
74
  require 'rudy/aws'
75
+ require 'rudy/config'
70
76
  require 'rudy/metadata'
71
- require 'rudy/scm/svn'
72
77
  require 'rudy/utils'
73
78
  require 'rudy/command/base'
74
79
 
75
- # Autoload Command and MetaData classes
80
+ # Require Command, MetaData, and SCM classes
76
81
  begin
77
- Dir.glob(File.join(RUDY_LIB, 'rudy', 'command', "*.rb")).each do |path|
78
- require path
79
- end
80
- Dir.glob(File.join(RUDY_LIB, 'rudy', 'metadata', "*.rb")).each do |path|
82
+ # TODO: Use autoload
83
+ Dir.glob(File.join(RUDY_LIB, 'rudy', '{command,metadata,scm}', "*.rb")).each do |path|
81
84
  require path
82
85
  end
83
86
  rescue LoadError => ex
@@ -93,7 +96,7 @@ end
93
96
  # end
94
97
  #
95
98
  def capture(stream)
96
- raise "We can only capture STDOUT or STDERR" unless stream == :stdout || stream == :stderr
99
+ #raise "We can only capture STDOUT or STDERR" unless stream == :stdout || stream == :stderr
97
100
 
98
101
  # I'm using this to trap the annoying right_aws "peer certificate" warning.
99
102
  # TODO: discover source of annoying right_aws warning and give it a hiding.
@@ -110,7 +113,19 @@ def capture(stream)
110
113
  end
111
114
 
112
115
 
116
+ def write_to_file(filename, content, type)
117
+ type = (type == :append) ? 'a' : 'w'
118
+ f = File.open(filename,type)
119
+ f.puts content
120
+ f.close
121
+ end
122
+
113
123
  def are_you_sure?(len=3)
124
+ if Drydock.debug?
125
+ puts 'DEBUG: skipping "are you sure" check'
126
+ return true
127
+ end
128
+
114
129
  if STDIN.tty? # Only ask a question if there's a human
115
130
  challenge = strand len
116
131
  STDOUT.print "Are you sure? To continue type \"#{challenge}\": "
@@ -137,26 +152,27 @@ def strand( len=8, safe=true )
137
152
  newpass
138
153
  end
139
154
 
140
- def sh(command, chdir=false)
155
+ def sh(command, chdir=false, verbose=false)
141
156
  prevdir = Dir.pwd
142
157
  Dir.chdir chdir if chdir
143
- puts command if @verbose > 0
158
+ puts command if verbose
144
159
  system(command)
145
160
  Dir.chdir prevdir if chdir
146
161
  end
147
162
 
148
- # TODO: Net::SSH
149
- def ssh(host, keypair, user, command=false, chdir=false, verbose=false, printonly=false)
150
- puts "CONNECTING TO #{host}..."
163
+ def ssh_command(host, keypair, user, command=false, printonly=false, verbose=false)
164
+ #puts "CONNECTING TO #{host}..."
151
165
  cmd = "ssh -q -i #{keypair} #{user}@#{host} "
152
- command = "cd #{chdir} && #{command}" if chdir
153
166
  cmd += " '#{command}'" if command
154
167
  puts cmd if verbose
155
- printonly ? (puts cmd) : system(cmd)
168
+ return cmd if printonly
169
+ # backticks returns STDOUT
170
+ # exec replaces current process (it's just like running ssh)
171
+ (command) ? `#{cmd}` : Kernel.exec(cmd)
156
172
  end
157
173
 
158
174
 
159
- def scp(host, keypair, user, paths, to_path, to_local=false, verbose=false, printonly=false)
175
+ def scp_command(host, keypair, user, paths, to_path, to_local=false, verbose=false, printonly=false)
160
176
 
161
177
  paths = [paths] unless paths.is_a?(Array)
162
178
  from_paths = ""
@@ -181,6 +197,13 @@ def scp(host, keypair, user, paths, to_path, to_local=false, verbose=false, prin
181
197
  end
182
198
 
183
199
 
200
+ # Returns +str+ with the average leading indentation removed.
201
+ # Useful for keeping inline codeblocks spaced with code.
202
+ def without_indent(str)
203
+ lines = str.split($/)
204
+ lspaces = (lines.inject(0) {|total,line| total += (line.scan(/^\s+/).first || '').size } / lines.size) + 1
205
+ lines.collect { |line| line.gsub(/^\s{#{lspaces}}/, '') }.join($/)
206
+ end
184
207
 
185
208
 
186
209
 
data/lib/rudy/aws/ec2.rb CHANGED
@@ -57,7 +57,16 @@ module Rudy::AWS
57
57
  class Volumes
58
58
  include Rudy::AWS::ObjectBase
59
59
 
60
-
60
+ # [{:aws_device=>"/dev/sdr",
61
+ # :aws_attachment_status=>"attached",
62
+ # :snapshot_id=>nil,
63
+ # :aws_id=>"vol-6811f601",
64
+ # :aws_attached_at=>Wed Mar 11 07:06:44 UTC 2009,
65
+ # :aws_status=>"in-use",
66
+ # :aws_instance_id=>"i-0b2ab662",
67
+ # :aws_created_at=>Tue Mar 10 18:55:18 UTC 2009,
68
+ # :zone=>"us-east-1b",
69
+ # :aws_size=>10}]
61
70
  def list
62
71
  list = @aws.describe_volumes() || []
63
72
  list.select { |v| v[:aws_status] != "deleting" }
@@ -86,7 +95,29 @@ module Rudy::AWS
86
95
  false
87
96
  end
88
97
 
98
+ def get(vol_id)
99
+ list = @aws.describe_volumes(vol_id) || []
100
+ list.first
101
+ end
102
+
103
+ def deleting?(vol_id)
104
+ return false unless vol_id
105
+ vol = get(vol_id)
106
+ (vol && vol[:aws_status] == "deleting")
107
+ end
89
108
 
109
+ def available?(vol_id)
110
+ return false unless vol_id
111
+ vol = get(vol_id)
112
+ (vol && vol[:aws_status] == "available")
113
+ end
114
+
115
+ def attached?(vol_id)
116
+ return false unless vol_id
117
+ vol = get(vol_id)
118
+ (vol && vol[:aws_status] == "in-use")
119
+ end
120
+
90
121
  end
91
122
 
92
123
  class Instances
@@ -95,11 +126,15 @@ module Rudy::AWS
95
126
  def destroy(*list)
96
127
  begin
97
128
  @aws.terminate_instances(list.flatten)
98
- rescue RightAws::AwsError => ex
99
- raise UnknownInstance.new
129
+ #rescue RightAws::AwsError => ex
130
+ # raise UnknownInstance.new
100
131
  end
101
132
  end
102
133
 
134
+ def restart(*list)
135
+ @aws.reboot_instances(list.flatten)
136
+ end
137
+
103
138
  def attached_volume?(id, device)
104
139
  list = volumes(id)
105
140
  list.each do |v|
@@ -113,6 +148,10 @@ module Rudy::AWS
113
148
  list.select { |v| v[:aws_status] != "deleting" && v[:aws_instance_id] === id }
114
149
  end
115
150
 
151
+ def device_volume(id, device)
152
+ volumes.select { |v| v[:aws_device] === device }
153
+ end
154
+
116
155
  def create(ami, group, keypair_name, user_data, zone)
117
156
  @aws.run_instances(ami, 1, 1, [group], keypair_name, user_data, 'public', nil, nil, nil, zone)
118
157
  end
@@ -121,6 +160,24 @@ module Rudy::AWS
121
160
  # that matches +filter+.
122
161
  # Returns a hash. The keys are instance IDs and the values are a hash
123
162
  # of attributes associated to that instance.
163
+ # {:aws_state_code=>"16",
164
+ # :private_dns_name=>"domU-12-31-38-00-51-F1.compute-1.internal",
165
+ # :aws_instance_type=>"m1.small",
166
+ # :aws_reason=>"",
167
+ # :ami_launch_index=>"0",
168
+ # :aws_owner=>"207436219441",
169
+ # :aws_launch_time=>"2009-03-11T06:55:00.000Z",
170
+ # :aws_kernel_id=>"aki-a71cf9ce",
171
+ # :ssh_key_name=>"rilli-sexytime",
172
+ # :aws_reservation_id=>"r-66f5710f",
173
+ # :aws_state=>"running",
174
+ # :aws_ramdisk_id=>"ari-a51cf9cc",
175
+ # :aws_instance_id=>"i-0b2ab662",
176
+ # :aws_groups=>["rudydev-app"],
177
+ # :aws_availability_zone=>"us-east-1b",
178
+ # :aws_image_id=>"ami-daca2db3",
179
+ # :aws_product_codes=>[],
180
+ # :dns_name=>"ec2-67-202-9-30.compute-1.amazonaws.com"}
124
181
  def list(filter='.')
125
182
  filter = filter.to_s.downcase.tr('_|-', '.') # treat dashes, underscores as one
126
183
  # Returns an array of hashes with the following keys:
@@ -138,6 +195,7 @@ module Rudy::AWS
138
195
  end
139
196
 
140
197
  def get(inst_id)
198
+ # This is ridiculous. Send inst_id to describe volumes
141
199
  instance = {}
142
200
  list.each_pair do |id, hash|
143
201
  next unless inst_id == id
@@ -178,8 +236,8 @@ module Rudy::AWS
178
236
 
179
237
  # Create a new EC2 security group
180
238
  # Returns true/false whether successful
181
- def create(name, desc='')
182
- @aws.create_security_group(name, desc)
239
+ def create(name, desc=nil)
240
+ @aws.create_security_group(name, desc || "Group #{name}")
183
241
  end
184
242
 
185
243
  # Delete an EC2 security group
@@ -4,25 +4,31 @@
4
4
  module Rudy
5
5
  module Command
6
6
  class Addresses < Rudy::Command::Base
7
-
8
- def associate_address(address)
9
- raise "You did not supply an instance ID" unless instance
10
- inst = @ec2.instances.get(instance)
11
- raise "Instance #{inst[:aws_instance_id]} is not running!" unless inst
7
+
8
+
9
+ def associate_addresses_valid?
10
+ raise "You have not supplied an IP addresses" unless @argv.address
11
+ raise "You did not supply an instance ID" unless @argv.instanceid
12
+
13
+ @inst = @ec2.instances.get(@argv.instanceid)
14
+ raise "Instance #{@inst[:aws_instance_id]} does not exist!" unless @inst
12
15
 
13
- raise "You have not supplied an IP addresses" unless address
14
- raise "That's not an elastic IP you own!" unless @ec2.addresses.valid?(address)
15
- raise "#{address} is already associated!" if @ec2.addresses.associated?(address)
16
+ raise "That's not an elastic IP you own!" unless @ec2.addresses.valid?(@argv.address)
17
+ raise "#{@argv.address} is already associated!" if @ec2.addresses.associated?(@argv.address)
16
18
 
17
- puts "Associating #{address} to #{inst[:aws_groups]}: #{inst[:dns_name]}"
18
- @ec2.addresses.associate(inst[:aws_instance_id], address)
19
+ true
20
+ end
21
+
22
+ def associate_addresses
23
+ puts "Associating #{@argv.address} to #{@inst[:aws_groups]}: #{@inst[:dns_name]}"
24
+ @ec2.addresses.associate(@inst[:aws_instance_id], @argv.address)
19
25
  puts "Done!"
20
26
  puts
21
27
 
22
- print_addresses
28
+ addresses
23
29
  end
24
30
 
25
- def print_addresses
31
+ def addresses
26
32
  puts "Elastic IP mappings:"
27
33
  @ec2.addresses.list.each do |address|
28
34
  print "IP: #{address[:public_ip]} "
@@ -34,7 +40,6 @@ module Rudy
34
40
  puts
35
41
  end
36
42
 
37
-
38
43
  end
39
44
  end
40
45
  end
@@ -0,0 +1,175 @@
1
+
2
+
3
+
4
+
5
+ module Rudy
6
+ module Command
7
+ class Backups < Rudy::Command::Base
8
+
9
+
10
+ def backup
11
+ criteria = [@global.zone]
12
+ criteria += [@global.environment, @global.role] unless @option.all
13
+
14
+ Rudy::MetaData::Backup.list(@sdb, *criteria).each do |backup|
15
+ puts "%s (%s)" % [backup.name, backup.awsid]
16
+ end
17
+ end
18
+
19
+ # Check for backups pointing to snapshots that don't exist.
20
+ def sync_backup
21
+ unless argv.empty?
22
+ puts "The disk you specified will be ignored."
23
+ argv.clear
24
+ end
25
+
26
+ criteria = [@global.zone]
27
+ criteria += [@global.environment, @global.role] unless @option.all
28
+
29
+ puts "Looking for backup metadata with delinquent snapshots..."
30
+ to_be_deleted = {} # snap-id => backup
31
+ Rudy::MetaData::Backup.list(@sdb, *criteria).each do |backup|
32
+ to_be_deleted[backup.awsid] = backup unless @ec2.snapshots.exists?(backup.awsid)
33
+ end
34
+
35
+ if to_be_deleted.empty?
36
+ puts "All backups are in-sync with snapshots. Nothing to do."
37
+ return
38
+ end
39
+
40
+ puts
41
+ puts "These backup metadata will be deleted:"
42
+ to_be_deleted.each do |snap_id, backup|
43
+ puts "%s: %s" % [snap_id, backup.name]
44
+ end
45
+
46
+ puts
47
+ are_you_sure?
48
+
49
+ puts
50
+ puts "Deleting..."
51
+ to_be_deleted.each do |snap_id, backup|
52
+ print " -> #{backup.name}... "
53
+ @sdb.destroy(RUDY_DOMAIN, backup.name)
54
+ puts "done"
55
+ end
56
+
57
+ puts "Done!"
58
+ end
59
+
60
+ def destroy_backup_valid?
61
+ raise "No backup specified" if argv.empty?
62
+ exit unless are_you_sure?(5)
63
+ true
64
+ end
65
+
66
+ def destroy_backup
67
+ name = @argv.first
68
+ puts "Destroying #{name}"
69
+ begin
70
+ backup = Rudy::MetaData::Backup.get(@sdb, name)
71
+ rescue => ex
72
+ puts "Error deleteing backup: #{ex.message}"
73
+ end
74
+
75
+ return unless backup
76
+
77
+ begin
78
+ puts " -> deleting snapshot..."
79
+ @ec2.snapshots.destroy(backup.awsid)
80
+ rescue => ex
81
+ puts "Error deleting snapshot: #{ex.message}."
82
+ puts "Continuing..."
83
+ ensure
84
+ puts " -> deleting metadata..."
85
+ @sdb.destroy(RUDY_DOMAIN, name)
86
+ end
87
+ puts "Done."
88
+ end
89
+
90
+ def create_backup
91
+ diskname = @argv.first
92
+
93
+ machine = find_current_machine
94
+
95
+ disks = Rudy::MetaData::Disk.list(@sdb, machine[:aws_availability_zone], @global.environment, @global.role, @global.position)
96
+ raise "The machine #{machine_name} does not have any disk metadata" if disks.empty?
97
+
98
+ puts "Machine: #{machine_name}"
99
+
100
+ if @option.snapshot
101
+ raise "You must supply a diskname when using an existing snapshot" unless diskname
102
+ raise "The snapshot #{@option.snapshot} does not exist" unless @ec2.snapshots.exists?(@option.snapshot)
103
+ disk = Rudy::MetaData::Disk.get(@sdb, diskname)
104
+
105
+ raise "The disk #{diskname} does not exist" unless disk
106
+ backup = Rudy::MetaData::Backup.new
107
+ backup.awsid = @option.snapshot
108
+ backup.time_stamp
109
+
110
+ # Populate machine infos
111
+ [:zone, :environment, :role, :position].each do |n|
112
+ backup.send("#{n}=", @global.send(n)) if @global.send(n)
113
+ end
114
+
115
+ # Populate disk infos
116
+ [:path, :size].each do |n|
117
+ backup.send("#{n}=", disk.send(n)) if disk.send(n)
118
+ end
119
+
120
+
121
+ Rudy::MetaData::Backup.save(@sdb, backup)
122
+
123
+ puts backup.name
124
+
125
+ else
126
+ volumes = @ec2.instances.volumes(machine[:aws_instance_id])
127
+ raise "The machine #{machine_name} does not have any volumes attached." if volumes.empty?
128
+
129
+ puts "#{disks.size} Disk(s) defined with #{volumes.size} Volume(s) running"
130
+
131
+ volumes.each do |volume|
132
+ print "Volume #{volume[:aws_id]}... "
133
+ disk = Rudy::MetaData::Disk.find_from_volume(@sdb, volume[:aws_id])
134
+ backup = Rudy::MetaData::Backup.new
135
+
136
+ # TODO: Look for the disk based on the machine
137
+ raise "No disk associated to volume #{volume[:aws_id]}" unless disk
138
+
139
+ backup.volume = volume[:aws_id]
140
+
141
+ # Populate machine infos
142
+ [:zone, :environment, :role, :position].each do |n|
143
+ backup.send("#{n}=", @global.send(n)) if @global.send(n)
144
+ end
145
+
146
+ # Populate disk infos
147
+ [:path, :size].each do |n|
148
+ backup.send("#{n}=", disk.send(n)) if disk.send(n)
149
+ end
150
+
151
+ backup.time_stamp
152
+
153
+ raise "There was a problem creating the backup metadata" unless backup.valid?
154
+
155
+ snap = @ec2.snapshots.create(volume[:aws_id])
156
+
157
+ if !snap || !snap.is_a?(Hash)
158
+ puts "There was an unknown problem creating #{backup.name}. Continuing with the next volume..."
159
+ next
160
+ end
161
+
162
+ backup.awsid = snap[:aws_id]
163
+
164
+ Rudy::MetaData::Backup.save(@sdb, backup)
165
+
166
+ puts backup.name
167
+
168
+ end
169
+ end
170
+ end
171
+
172
+
173
+ end
174
+ end
175
+ end