rudy 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +21 -0
- data/LICENSE.txt +19 -0
- data/README.rdoc +32 -0
- data/Rakefile +68 -0
- data/bin/rudy +258 -0
- data/lib/drydock.rb +525 -0
- data/lib/rudy.rb +102 -0
- data/lib/rudy/aws.rb +65 -0
- data/lib/rudy/aws/ec2.rb +197 -0
- data/lib/rudy/aws/s3.rb +3 -0
- data/lib/rudy/aws/simpledb.rb +48 -0
- data/lib/rudy/command/addresses.rb +41 -0
- data/lib/rudy/command/base.rb +275 -0
- data/lib/rudy/command/commit.rb +10 -0
- data/lib/rudy/command/disks.rb +61 -0
- data/lib/rudy/command/environment.rb +95 -0
- data/lib/rudy/command/groups.rb +59 -0
- data/lib/rudy/command/images.rb +61 -0
- data/lib/rudy/command/instances.rb +109 -0
- data/lib/rudy/command/metadata.rb +57 -0
- data/lib/rudy/command/release.rb +43 -0
- data/lib/rudy/command/volumes.rb +13 -0
- data/lib/rudy/metadata/disk.rb +142 -0
- data/lib/rudy/metadata/environment.rb +0 -0
- data/lib/rudy/scm/svn.rb +57 -0
- data/lib/rudy/utils.rb +65 -0
- data/lib/storable.rb +268 -0
- data/rudy.gemspec +52 -0
- data/support/rudy-ec2-startup +166 -0
- metadata +87 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Rudy
|
4
|
+
module Command
|
5
|
+
class Images < Rudy::Command::Base
|
6
|
+
|
7
|
+
|
8
|
+
def print_images
|
9
|
+
@ec2.images.list.each do |img|
|
10
|
+
print_image img
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_image
|
15
|
+
|
16
|
+
if @user != "root"
|
17
|
+
puts "This command will be run as root"
|
18
|
+
@user = "root" # We need to be root for this operation!
|
19
|
+
end
|
20
|
+
|
21
|
+
raise "No EC2 .pem keys provided" unless has_pem_keys?
|
22
|
+
raise "No SSH key provided for #{keypairname}!" unless has_keypair?(keypairname)
|
23
|
+
raise "SSH key provided but cannot be found! (#{keypairpath})" unless File.exists?(keypairpath)
|
24
|
+
|
25
|
+
machine_list = @ec2.instances.list(machine_group)
|
26
|
+
machine = machine_list.values.first # NOTE: Only one machine per group, for now...
|
27
|
+
|
28
|
+
raise "There's no machine running in #{machine_group}" unless machine
|
29
|
+
raise "The primary machine in #{machine_group} is not in a running state" unless machine[:aws_state] == 'running'
|
30
|
+
|
31
|
+
puts "The new image will be based on #{machine_group}_01"
|
32
|
+
|
33
|
+
unless @account
|
34
|
+
puts "Enter your 12 digit Amazon account number:"
|
35
|
+
@account = gets.chomp
|
36
|
+
end
|
37
|
+
|
38
|
+
unless @image_name
|
39
|
+
puts "Enter the image name:"
|
40
|
+
@image_name = gets.chomp
|
41
|
+
end
|
42
|
+
|
43
|
+
unless @bucket_name
|
44
|
+
puts "Enter the S3 bucket that will store the image:"
|
45
|
+
@bucket_name = gets.chomp
|
46
|
+
end
|
47
|
+
|
48
|
+
scp machine[:dns_name], keypairpath, user, @ec2_cert, "/mnt/"
|
49
|
+
scp machine[:dns_name], keypairpath, user, @ec2_private_key, "/mnt/"
|
50
|
+
|
51
|
+
ssh machine[:dns_name], keypairpath, user, "ec2-bundle-vol -r i386 -p #{@image_name} -k /mnt/pk-*pem -c /mnt/cert*pem -u #{@account}"
|
52
|
+
ssh machine[:dns_name], keypairpath, user, "ec2-upload-bundle -b #{@bucket_name} -m /tmp/#{@image_name}.manifest.xml -a #{@access_key} -s #{@secret_key}"
|
53
|
+
|
54
|
+
# Why did I not use the Ruby API?
|
55
|
+
sh "ec2-register -K #{@ec2_private_key} -C #{@ec2_cert} #{@bucket_name}/#{@image_name}.manifest.xml"
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,109 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Rudy
|
4
|
+
module Command
|
5
|
+
class Instances < Rudy::Command::Base
|
6
|
+
|
7
|
+
def print_instances(filter=nil)
|
8
|
+
filter = machine_group if filter.nil? && !@all
|
9
|
+
if instance_id?(filter)
|
10
|
+
inst = @ec2.instances.get(filter)
|
11
|
+
raise "The instance #{filter} does not exist" if inst.empty?
|
12
|
+
list = {inst[:aws_instance_id] => inst}
|
13
|
+
else
|
14
|
+
raise "The security group #{filter} does not exist" if filter && !@ec2.groups.exists?(filter)
|
15
|
+
list = @ec2.instances.list(filter)
|
16
|
+
if list.empty?
|
17
|
+
msg = "There are no instances running"
|
18
|
+
msg << " in the group #{filter}" if filter
|
19
|
+
raise msg
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
list.each_pair do |id, inst|
|
24
|
+
print_instance inst
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def destroy_instances(filter=nil)
|
30
|
+
raise "No instance ID or group name provided" if filter.nil?
|
31
|
+
raise "I will not help you destroy production!" if @environment == "prod" || filter =~ /^prod/
|
32
|
+
if @ec2.groups.exists?(filter)
|
33
|
+
list = @ec2.instances.list(filter)
|
34
|
+
raise "The group #{filter} has no running instances" if list.empty?
|
35
|
+
instance = list.keys.first
|
36
|
+
else
|
37
|
+
instance = filter
|
38
|
+
end
|
39
|
+
puts "Destroying #{instance}!"
|
40
|
+
@ec2.instances.destroy instance
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_instance
|
44
|
+
@image ||= machine_image
|
45
|
+
|
46
|
+
@user = "root"
|
47
|
+
|
48
|
+
rig = @ec2.instances.list(machine_group)
|
49
|
+
raise "There is already an instance running in #{machine_group}" unless rig.empty?
|
50
|
+
raise "No SSH key provided for #{keypairname}!" unless has_keypair?(keypairname)
|
51
|
+
raise "SSH key provided but cannot be found! (#{keypairpath})" unless File.exists?(keypairpath)
|
52
|
+
|
53
|
+
user_data = {
|
54
|
+
:dbmaster => "localhost",
|
55
|
+
:role => @role,
|
56
|
+
:env => @environment
|
57
|
+
}
|
58
|
+
|
59
|
+
puts "Starting an instance in #{machine_group}"
|
60
|
+
instances = @ec2.instances.create(@image, machine_group.to_s, File.basename(keypairpath), user_data.to_yaml, @zone)
|
61
|
+
inst = instances.first
|
62
|
+
id, state = inst[:aws_instance_id], inst[:aws_state]
|
63
|
+
|
64
|
+
if @address
|
65
|
+
puts "Associating #{@address} to #{id}"
|
66
|
+
@ec2.addresses.associate(id, @address)
|
67
|
+
end
|
68
|
+
|
69
|
+
print "Waiting for #{id} to become available"
|
70
|
+
|
71
|
+
while @ec2.instances.pending?(id)
|
72
|
+
sleep 2
|
73
|
+
print '.'
|
74
|
+
STDOUT.flush
|
75
|
+
end
|
76
|
+
|
77
|
+
machine = @ec2.instances.get(id)
|
78
|
+
|
79
|
+
puts " It's up!"
|
80
|
+
print "Waiting for SSH daemon at #{machine[:dns_name]}"
|
81
|
+
while !Rudy::Utils.service_available?(machine[:dns_name], 22)
|
82
|
+
print '.'
|
83
|
+
STDOUT.flush
|
84
|
+
end
|
85
|
+
puts " It's up!"
|
86
|
+
|
87
|
+
print "Looking for disk metadata for #{machine[:aws_availability_zone]}... "
|
88
|
+
disks = Rudy::MetaData::Disk.list(@sdb, machine[:aws_availability_zone], @environment, @role, @position)
|
89
|
+
|
90
|
+
if disks.empty?
|
91
|
+
puts "None"
|
92
|
+
else
|
93
|
+
puts "#{disks.size} disk(s)."
|
94
|
+
disks.each do |disk|
|
95
|
+
|
96
|
+
do_dirty_disk_volume_deeds(disk, machine)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
puts
|
101
|
+
ssh machine[:dns_name], keypairpath, user, "df -h" # Display current mounts
|
102
|
+
puts
|
103
|
+
puts "Done!"
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Rudy
|
4
|
+
module Command
|
5
|
+
class Metadata < Rudy::Command::Base
|
6
|
+
|
7
|
+
|
8
|
+
# Update Rudy's metadata for a running instance
|
9
|
+
def update_metadata(instances)
|
10
|
+
attributes = {
|
11
|
+
'role' => role,
|
12
|
+
'environment' => environment,
|
13
|
+
'instances' => instances
|
14
|
+
}
|
15
|
+
puts "Putting #{instances.join(', ')} into #{machine_group}"
|
16
|
+
|
17
|
+
@sdb.store(RUDY_DOMAIN, "group_#{machine_group}", attributes)
|
18
|
+
|
19
|
+
p group_metadata
|
20
|
+
end
|
21
|
+
|
22
|
+
# Print Rudy's metadata to STDOUT
|
23
|
+
def print_metadata(instances=[])
|
24
|
+
p group_metadata
|
25
|
+
#query = "['instances' = '#{instances.first}'] union ['group' = '#{machine}']"
|
26
|
+
#
|
27
|
+
# p @sdb.get_attributes(RUDY_DOMAIN, "instances_#{machine}")
|
28
|
+
# p @sdb.query(RUDY_DOMAIN, query)
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy_metadata
|
32
|
+
@sdb.domains.destroy(RUDY_DOMAIN)
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup
|
36
|
+
puts "Checking environment..."
|
37
|
+
check_environment
|
38
|
+
|
39
|
+
puts "Creating SimpleDB domain called #{RUDY_DOMAIN}"
|
40
|
+
@sdb.domains.create(RUDY_DOMAIN)
|
41
|
+
end
|
42
|
+
|
43
|
+
def info
|
44
|
+
domains = @sdb.domains.list[:domains]
|
45
|
+
puts "Domains: #{domains.join(", ")}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def check_environment
|
49
|
+
raise "No Amazon keys provided!" unless has_keys?
|
50
|
+
raise "No SSH keypairs provided!" unless has_keypairs?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Rudy
|
4
|
+
module Command
|
5
|
+
class Release < Rudy::Command::Base
|
6
|
+
|
7
|
+
|
8
|
+
def create_release
|
9
|
+
|
10
|
+
if @user != "root"
|
11
|
+
#puts "This command will be run as root"
|
12
|
+
@user = "root" # We need to be root for this operation!
|
13
|
+
end
|
14
|
+
|
15
|
+
raise "No EC2 .pem keys provided" unless has_pem_keys?
|
16
|
+
raise "No SSH key provided for #{keypairname}!" unless has_keypair?(keypairname)
|
17
|
+
raise "SSH key provided but cannot be found! (#{keypairpath})" unless File.exists?(keypairpath)
|
18
|
+
|
19
|
+
raise "The security group #{filter} does not exist" if machine_group && !@ec2.groups.exists?(machine_group)
|
20
|
+
list = @ec2.instances.list(machine_group)
|
21
|
+
raise "Please start an instance in #{machine_group} before releasing!" if list.empty?
|
22
|
+
puts "Creating release from: #{Dir.pwd}"
|
23
|
+
|
24
|
+
tag = @scm.create_release
|
25
|
+
|
26
|
+
machine = list.values.first # NOTE: we're assuming there's only one machine
|
27
|
+
|
28
|
+
#basename = File.basename(@rscripts[keypairname])
|
29
|
+
#scp machine[:dns_name], keypairpath, user, @rscripts[keypairname], "/mnt/"
|
30
|
+
#ssh machine[:dns_name], keypairpath, user, "chmod 755 /mnt/#{basename} && /mnt/#{basename} #{tag}"
|
31
|
+
|
32
|
+
@user = "rudy"
|
33
|
+
basename = File.basename(@rscripts[keypairname])
|
34
|
+
|
35
|
+
scp machine[:dns_name], keypairpath, user, @rscripts[keypairname], "~/"
|
36
|
+
ssh machine[:dns_name], keypairpath, user, "chmod 755 ~/#{basename} && ~/#{basename} test #{@access_key} #{@secret_key} r1ll1r1ll1"
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,142 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Rudy
|
4
|
+
class MetaData
|
5
|
+
attr_accessor :sdb
|
6
|
+
|
7
|
+
def initalize(sdb)
|
8
|
+
@sdb = sdb
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
class MetaData
|
13
|
+
class Disk < Storable
|
14
|
+
|
15
|
+
field :awsid
|
16
|
+
|
17
|
+
field :environment
|
18
|
+
field :role
|
19
|
+
field :path
|
20
|
+
field :position
|
21
|
+
|
22
|
+
field :zone
|
23
|
+
field :region
|
24
|
+
field :device
|
25
|
+
field :backups => Array
|
26
|
+
field :size
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@device = "/dev/sdh"
|
30
|
+
@zone = DEFAULT_ZONE
|
31
|
+
@region = DEFAULT_REGION
|
32
|
+
@backups = []
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def name
|
37
|
+
Disk.generate_name(@zone, @environment, @role, @position, @path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid?
|
41
|
+
@zone && @environment && @role && @position && @path
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_query(more=[], remove=[])
|
45
|
+
criteria = [:zone, :environment, :role, :position, :path, *more]
|
46
|
+
criteria -= [*remove].flatten
|
47
|
+
query = []
|
48
|
+
criteria.each do |n|
|
49
|
+
query << "['#{n}' = '#{self.send(n.to_sym)}']"
|
50
|
+
end
|
51
|
+
query.join(" intersection ")
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
str = ""
|
56
|
+
field_names.each do |key|
|
57
|
+
str << sprintf(" %22s: %s#{$/}", key, self.send(key.to_sym))
|
58
|
+
end
|
59
|
+
str
|
60
|
+
end
|
61
|
+
|
62
|
+
def Disk.generate_name(zon, env, rol, pos, pat, sep=File::SEPARATOR)
|
63
|
+
pos = pos.to_s.rjust 2, '0'
|
64
|
+
dirs = pat.split sep if pat
|
65
|
+
dirs.shift while dirs && (dirs[0].nil? || dirs[0].empty?)
|
66
|
+
["disk", zon, env, rol, pos, *dirs].join(RUDY_DELIM)
|
67
|
+
end
|
68
|
+
|
69
|
+
def Disk.get(sdb, name)
|
70
|
+
disk = sdb.get_attributes(RUDY_DOMAIN, name)
|
71
|
+
raise "Disk #{name} does not exist!" unless disk && disk.has_key?(:attributes)
|
72
|
+
Rudy::MetaData::Disk.from_hash(disk[:attributes])
|
73
|
+
end
|
74
|
+
|
75
|
+
def Disk.destroy(sdb, name)
|
76
|
+
disk = Disk.get(sdb, name) # get raises an exception if the disk doesn't exist
|
77
|
+
sdb.destroy(RUDY_DOMAIN, name)
|
78
|
+
true # wtf: RightAws::SimpleDB doesn't tell us whether it succeeds. We'll assume!
|
79
|
+
end
|
80
|
+
|
81
|
+
def Disk.save(sdb, disk)
|
82
|
+
sdb.store(RUDY_DOMAIN, disk.name, disk.to_hash, :replace)
|
83
|
+
end
|
84
|
+
|
85
|
+
def Disk.is_defined?(sdb, disk)
|
86
|
+
# We don't care about the path, but we do care about the device
|
87
|
+
# which is not part of the disk's name.
|
88
|
+
query = disk.to_query(:device, :path)
|
89
|
+
!sdb.query_with_attributes(RUDY_DOMAIN, query).empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def Disk.update_volume(sdb, ec2, disk, machine)
|
93
|
+
|
94
|
+
disk = Disk.get(sdb, disk) if disk.is_a?(String)
|
95
|
+
raise "You must provide a disk name or obect" unless disk.is_a?(Rudy::MetaData::Disk)
|
96
|
+
|
97
|
+
|
98
|
+
# Make sure the volume is still running
|
99
|
+
disk.awsid = nil if disk.awsid && !ec2.volumes.exists?(disk.awsid)
|
100
|
+
|
101
|
+
|
102
|
+
# Otherwise we need to start one
|
103
|
+
unless disk.awsid
|
104
|
+
puts "No active EBS volume found for #{disk.name}"
|
105
|
+
|
106
|
+
backup = disk.backups.last
|
107
|
+
if backup
|
108
|
+
puts "We'll use the most recent backup..."
|
109
|
+
volume = ec2.volumes.create(disk.zone, disk.size, backup)
|
110
|
+
puts "Attaching #{disk.awsid} to #{id} (#{disk.device})"
|
111
|
+
ec2.volumes.attach(machine[:aws_instance_id], disk.awsid, disk.device)
|
112
|
+
else
|
113
|
+
puts "We'll create one from scratch..."
|
114
|
+
volume = ec2.volumes.create(disk.zone, disk.size, nil)
|
115
|
+
disk.awsid = volume[:aws_id]
|
116
|
+
puts "Saving disk metadata"
|
117
|
+
Disk.save(sdb, disk)
|
118
|
+
end
|
119
|
+
puts ""
|
120
|
+
end
|
121
|
+
|
122
|
+
disk
|
123
|
+
end
|
124
|
+
|
125
|
+
def Disk.list(sdb, zon, env=nil, rol=nil, pos=nil)
|
126
|
+
query = ''
|
127
|
+
query << "['zone' = '#{zon}']" if zon
|
128
|
+
query << " intersection ['environment' = '#{env}']" if env
|
129
|
+
query << " intersection ['role' = '#{rol}']" if rol
|
130
|
+
query << " intersection ['position' = '#{pos}']" if pos
|
131
|
+
|
132
|
+
list = []
|
133
|
+
sdb.query_with_attributes(RUDY_DOMAIN, query).each_pair do |name, hash|
|
134
|
+
list << Rudy::MetaData::Disk.from_hash(hash)
|
135
|
+
end
|
136
|
+
list
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
File without changes
|
data/lib/rudy/scm/svn.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Rudy
|
5
|
+
module SCM
|
6
|
+
class SVN
|
7
|
+
attr_accessor :base_uri
|
8
|
+
|
9
|
+
def initialize(uri)
|
10
|
+
@base_uri = uri
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_release
|
14
|
+
raise "There are local changes. Please revert or check them in!" unless everything_checked_in?
|
15
|
+
raise "Invalid base URI (#{@base_uri}). Check RUDY_SVN_BASE." unless valid_uri?(@base_uri)
|
16
|
+
raise "You must run this command from SVN you want to release from!" unless svn_dir?(Dir.pwd)
|
17
|
+
|
18
|
+
re = `svn info`.match /^URL:\s+(.+)$/
|
19
|
+
release = re[1] if re
|
20
|
+
|
21
|
+
release_tag = "#{@base_uri}/#{generate_release_tag}"
|
22
|
+
|
23
|
+
puts "Creating tag: #{release_tag}"
|
24
|
+
cmd = "svn copy -m 'Another Release by Rudy!' #{release} #{release_tag}"
|
25
|
+
|
26
|
+
`#{cmd}`
|
27
|
+
|
28
|
+
release_tag
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate_release_tag
|
32
|
+
now = Time.now
|
33
|
+
mon = now.mon.to_s.rjust(2, '0')
|
34
|
+
day = now.day.to_s.rjust(2, '0')
|
35
|
+
rev = "r01"
|
36
|
+
criteria = ['rudy', now.year, mon, day, rev]
|
37
|
+
tag = criteria.join(RUDY_DELIM)
|
38
|
+
# Keep incrementing the revision number until we find the next one.
|
39
|
+
tag.succ! while (valid_uri?("#{@base_uri}/#{tag}"))
|
40
|
+
tag
|
41
|
+
end
|
42
|
+
|
43
|
+
def svn_dir?(path)
|
44
|
+
(File.exists?(File.join(path, '.svn')))
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_uri?(uri)
|
48
|
+
ret = `svn info #{uri} 2>1&` || '' # Valid SVN URIs will return some info
|
49
|
+
(ret =~ /Repository UUID/) ? true : false
|
50
|
+
end
|
51
|
+
|
52
|
+
def everything_checked_in?
|
53
|
+
`svn diff . 2>1&` == '' # svn diff should return nothing
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|