rudy 0.3.2 → 0.4.0

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