boucher 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -119,11 +119,13 @@ allows you too add extra configuration information under the "Boucher": key. Fo
119
119
  "groups": ["SSH"], // overides :default_groups config
120
120
  "key_name": ["some_key"], // overides :aws_key_filename config
121
121
  "elastic_ips": ["1.2.3.4"], // a list of elastic IPs that'll be attached to the server. Elastic IP's acquired via AWS management console.
122
- "volumes": ["some_volume"] // a list of volume names that'll be attached to the server. Volumes acquired via AWS management console.
122
+ "volumes": {"/dev/sda2": <volume spec>} // See Volume Specs below
123
123
  }
124
124
  }
125
125
 
126
- ERB: The "boucher": content can contain ERB. So you can use config params like so:
126
+ ### ERB in config
127
+
128
+ Meal .json files may contain ERB in the "boucher" section. However, the file get's parsed by chef-solo so it has to remain a valid JSON file. But you can do things like this:
127
129
 
128
130
  {
129
131
  "run_list": ...
@@ -133,6 +135,28 @@ ERB: The "boucher": content can contain ERB. So you can use config params like
133
135
  }
134
136
  }
135
137
 
138
+ Also keep in mind that you can use ERB in recipes' template files.
139
+
140
+ ### Volume Specs
141
+
142
+ Volumes may be specified in the config for a given meal. The "volumes": entry must be a hash where keys are the device name (mount point) and the values
143
+ are a hash describing the volume. There are really three variations:
144
+
145
+ 1) Mounting an existing volume by using the volume_id:
146
+
147
+ "volumes": {"/dev/sda3" => {"volume_id": "volume-abc123"}}
148
+
149
+ 2) Mount a new volume based on an existing snapshot:
150
+
151
+ "volumes": {"/dev/sda4" => {"snapshot_id": "snapshot-abc123"}}
152
+
153
+ 3) Mount a new volume of a given size:
154
+
155
+ "volumes": {"/dev/sda5" => {"size": 16}}
156
+
157
+ If volumes are not specified, AWS will apply the default volume setup in the management console.
158
+
159
+
136
160
  ### Environments
137
161
 
138
162
  Enviroments are configured in config/env/<env_name>.rb. The project template we checked out earlier only provides one: dev.
data/TODO.md ADDED
@@ -0,0 +1,2 @@
1
+ * security groups: More thought required
2
+ * EBS snapshots: More thought required
data/boucher.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "boucher"
5
- s.version = "0.1.8"
5
+ s.version = "0.2.0"
6
6
  s.authors = ["'Micah Micah'"]
7
7
  s.email = ["'micah@8thlight.com'"]
8
8
  s.homepage = "http://github.com/8thlight/boucher"
@@ -0,0 +1,90 @@
1
+ require 'boucher/compute'
2
+ require 'boucher/servers'
3
+
4
+ module Boucher
5
+
6
+ ADDRESS_TABLE_FORMAT = "%-15s %-12s\n"
7
+
8
+ def self.print_addresses(addresses)
9
+ puts
10
+ printf ADDRESS_TABLE_FORMAT, "Public IP", "Server ID"
11
+ puts ("-" * 29)
12
+
13
+ addresses.each do |address|
14
+ printf ADDRESS_TABLE_FORMAT,
15
+ address.public_ip,
16
+ address.server_id
17
+ end
18
+ end
19
+
20
+ ADDRESS_OVERVIEW_TABLE_FORMAT = "%12s %-15s %-12s\n"
21
+
22
+ def self.print_address_overview(addresses)
23
+ puts
24
+ printf ADDRESS_OVERVIEW_TABLE_FORMAT, "Meal", "Public IP", "Server ID"
25
+ puts ("-" * 43)
26
+
27
+ addresses.values.each do |address|
28
+ printf ADDRESS_OVERVIEW_TABLE_FORMAT,
29
+ address[:meal],
30
+ address[:ip],
31
+ address[:server_id]
32
+ end
33
+ end
34
+
35
+ def self.address_overview
36
+ ips = {}
37
+ Boucher.compute.addresses.each do |ip|
38
+ ips[ip.public_ip] = {ip: ip.public_ip, server_id: ip.server_id}
39
+ end
40
+ Boucher.meals.each do |name, meal|
41
+ (meal[:elastic_ips] || []).each do |ip|
42
+ if ip.nil? || ip.size == 0
43
+ # skip
44
+ elsif ips[ip]
45
+ ips[ip][:meal] = name
46
+ else
47
+ ips[ip] = {meal: name, ip: ip}
48
+ end
49
+ end
50
+ end
51
+ ips
52
+ end
53
+
54
+ def self.associate_addresses_for(meal, server)
55
+ ips = meal[:elastic_ips]
56
+ if ips.nil? || ips.empty?
57
+ puts "No Elastic IPs to associate for meal #{meal[:name]}."
58
+ return
59
+ end
60
+ ips.each do |ip|
61
+ address = Boucher.compute.addresses.get(ip)
62
+ if address
63
+ if address.server_id == server.id
64
+ puts "#{ip} already associated with #{meal[:name]}:#{server.id}"
65
+ else
66
+ puts "Associating #{ip} with #{meal[:name]}:#{server.id}"
67
+ address.server = server
68
+ end
69
+ else
70
+ puts "Elastic IP (#{ip}) not found. Skipping."
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.associate_all_addresses
76
+ meals = Boucher.meals
77
+ meals.each do |name, meal|
78
+ ips = meal[:elastic_ips]
79
+ if ips && ips.size > 0
80
+ begin
81
+ server = Boucher::Servers.find(meal: name, env: Boucher::Config[:env])
82
+ associate_addresses_for(meal, server)
83
+ rescue Boucher::Servers::NotFound => e
84
+ puts "Can't associate address to '#{name}' server because it can't be found."
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ end
@@ -54,19 +54,4 @@ module Boucher
54
54
  rescue Exception => e
55
55
  false
56
56
  end
57
-
58
- def self.change_server_state(server_id, command, new_state)
59
- print "#{command}-ing server #{server_id}..."
60
- server = compute.servers.get(server_id)
61
- server.send(command.to_sym)
62
- server.wait_for { print "."; state == new_state }
63
- puts
64
- Boucher.print_servers [server]
65
- puts
66
- puts "The server has been #{command}-ed."
67
- end
68
-
69
- def self.find_servers
70
- compute.servers
71
- end
72
57
  end
data/lib/boucher/env.rb CHANGED
@@ -5,7 +5,7 @@ module Boucher
5
5
  } unless defined?(Boucher::Config)
6
6
 
7
7
  def self.env_name
8
- ENV["BENV"] ? ENV["BENV"] : :dev
8
+ Boucher::Config[:env] || ENV["BENV"] || :dev
9
9
  end
10
10
 
11
11
  env_dir = File.expand_path("config/env")
data/lib/boucher/io.rb CHANGED
@@ -22,87 +22,4 @@ module Boucher
22
22
  end
23
23
  end
24
24
 
25
- SERVER_TABLE_FORMAT = "%-12s %-12s %-10s %-10s %-10s %-15s %-15s %-10s\n"
26
-
27
- def self.print_server_table_header
28
- puts
29
- printf SERVER_TABLE_FORMAT, "ID", "Environment", "Meal", "Creator", "State", "Public IP", "Private IP", "Inst. Size"
30
- puts ("-" * 120)
31
- end
32
-
33
- def self.print_server(server)
34
- printf SERVER_TABLE_FORMAT,
35
- server.id,
36
- (server.tags["Env"] || "???")[0...12],
37
- (server.tags["Meal"] || "???")[0...10],
38
- (server.tags["Creator"] || "???")[0...10],
39
- server.state,
40
- server.public_ip_address,
41
- server.private_ip_address,
42
- server.flavor_id
43
- end
44
-
45
- def self.print_servers(servers)
46
- print_server_table_header
47
- sorted_servers = servers.sort_by{|s| [s.tags["Env"] || "?",
48
- s.tags["Meal"] || "?"]}
49
- sorted_servers.each do |server|
50
- print_server(server) if server
51
- end
52
- puts
53
- end
54
-
55
- def self.print_volumes(volumes)
56
- id_sizes = []
57
- size_sizes = []
58
- state_sizes = []
59
- zone_sizes = []
60
- snapshot_sizes = []
61
-
62
- Array(volumes).each do |volume|
63
- id_sizes << volume.id.length
64
- size_sizes << volume.size.to_s.length
65
- state_sizes << volume.state.length
66
- zone_sizes << volume.availability_zone.length
67
- snapshot_sizes << volume.snapshot_id.to_s.length
68
- end
69
-
70
- id_length = id_sizes.max + 5
71
- size_length = size_sizes.max + 5
72
- state_length = state_sizes.max + 5
73
- zone_length = zone_sizes.max + 5
74
- snapshot_length = snapshot_sizes.max
75
-
76
- puts "ID#{" "*(id_length - 2)}Size#{" "*(size_length - 4)}State#{" "*(state_length - 5)}Zone#{" "*(zone_length - 4)}Snapshot"
77
- puts "-"*(id_length + size_length + state_length + zone_length + snapshot_length)
78
-
79
- Array(volumes).each do |volume|
80
- puts "#{volume.id}#{" "*(id_length - volume.id.length)}#{volume.size}GB#{" "*(size_length - volume.size.to_s.length - 2)}#{volume.state}#{" "*(state_length - volume.state.length)}#{volume.availability_zone}#{" "*(zone_length - volume.availability_zone.length)}#{volume.snapshot_id}#{" "*(snapshot_length - volume.snapshot_id.to_s.length)}"
81
- end
82
- end
83
-
84
- FILE_TABLE_FORMAT = "%-60s %-10s %-25s %-32s\n"
85
-
86
- def self.print_file_table_header
87
- puts
88
- printf FILE_TABLE_FORMAT, "Key", "Size", "Last Modified", "etag"
89
- puts ("-" * 150)
90
- end
91
-
92
- def self.print_file(file)
93
- printf FILE_TABLE_FORMAT,
94
- file.key,
95
- file.content_length,
96
- file.last_modified,
97
- file.etag
98
- end
99
-
100
- def self.print_files(files)
101
- print_file_table_header
102
- files.each do |file|
103
- print_file(file) if file
104
- end
105
- puts
106
- end
107
-
108
25
  end
@@ -2,6 +2,7 @@ require 'boucher/compute'
2
2
  require 'boucher/io'
3
3
  require 'boucher/servers'
4
4
  require 'boucher/volumes'
5
+ require 'boucher/addresses'
5
6
  require 'retryable'
6
7
 
7
8
  module Boucher
@@ -23,7 +24,7 @@ module Boucher
23
24
  if server.nil?
24
25
  Boucher.provision(meal)
25
26
  elsif server.state == "stopped"
26
- Boucher::Servers.start(server.id)
27
+ Boucher::Servers.start([server])
27
28
  server.reload
28
29
  Boucher.cook_meal_on_server(meal, server)
29
30
  else
@@ -36,28 +37,17 @@ module Boucher
36
37
  server = create_meal_server(meal)
37
38
  wait_for_server_to_boot(server)
38
39
  wait_for_server_to_accept_ssh(server)
39
- volumes = create_volumes(meal, server)
40
- attach_volumes(volumes, server)
40
+ attach_volumes(meal, server)
41
41
  cook_meal_on_server(meal, server)
42
42
  puts "\nThe new #{meal[:name]} server has been provisioned! id: #{server.id}"
43
43
  end
44
44
 
45
- def self.attach_elastic_ips(meal, server)
46
- puts "Attaching elastic IPs..."
47
- ips = meal[:elastic_ips] || []
48
-
49
- ips.each do |ip|
50
- puts "Associating #{server.id} with #{ip}"
51
- compute.associate_address(server.id, ip)
52
- end
53
- end
54
-
55
45
  private
56
46
 
57
47
  def self.cook_meal_on_server(meal, server)
58
48
  puts "Cooking meal '#{meal[:name]}' on server: #{server}"
59
49
  Boucher.cook_meal(server, meal[:name])
60
- attach_elastic_ips(meal, server)
50
+ associate_addresses_for(meal, server)
61
51
  end
62
52
 
63
53
  def self.wait_for_server_to_accept_ssh(server)
@@ -81,21 +71,28 @@ module Boucher
81
71
  server
82
72
  end
83
73
 
84
- def self.create_volumes(meal, server)
85
- Array(meal[:volumes]).map do |volume_name|
86
- attributes = Boucher.volume_configs[volume_name]
87
- snapshot = snapshots.get(attributes[:snapshot])
88
- puts "Creating volume from snapshot #{snapshot.id}..."
89
- Boucher::Volumes.create(server.availability_zone, snapshot, attributes[:device])
74
+ def self.attach_volumes(meal, server)
75
+ volumes = meal[:volumes]
76
+ return unless volumes && volumes.size > 0
77
+ puts "Attaching volumes..."
78
+ volumes.each do |device, spec|
79
+ volume = acquire_volume(spec, server)
80
+ print "Attaching volume #{volume.id} to #{server.id}..."
81
+ Boucher.compute.attach_volume(server.id, volume.id, device)
82
+ volume.wait_for { print "."; volume.state == "in-use" }
83
+ puts
90
84
  end
91
85
  end
92
86
 
93
- def self.attach_volumes(volumes, server)
94
- volumes.each do |volume|
95
- print "Attaching volume #{volume.id} to #{server.id}..."
96
- Boucher::Volumes.attach(volume, server)
97
- volume.wait_for { print "."; state == "in-use" }
98
- puts
87
+ def self.acquire_volume(spec, server)
88
+ if spec[:volume_id]
89
+ Boucher.compute.volumes.get(spec[:volume_id])
90
+ elsif spec[:snapshot_id]
91
+ puts "Creating volume based on snapshot: #{spec[:snapshot_id]}"
92
+ Boucher::Volumes.create(:snapshot_id => spec[:snapshot_id], :availability_zone => server.availability_zone)
93
+ else
94
+ puts "Creating new volume of size: #{spec[:size]}GB"
95
+ Boucher::Volumes.create(:size => spec[:size].to_i, :availability_zone => server.availability_zone)
99
96
  end
100
97
  end
101
98
  end
@@ -1,10 +1,45 @@
1
1
  require 'boucher/compute'
2
2
 
3
3
  module Boucher
4
+
5
+ SERVER_TABLE_FORMAT = "%-12s %-12s %-10s %-10s %-10s %-15s %-15s %-10s\n"
6
+
7
+ def self.print_server_table_header
8
+ puts
9
+ printf SERVER_TABLE_FORMAT, "ID", "Environment", "Meal", "Creator", "State", "Public IP", "Private IP", "Inst. Size"
10
+ puts ("-" * 107)
11
+ end
12
+
13
+ def self.print_server(server)
14
+ printf SERVER_TABLE_FORMAT,
15
+ server.id,
16
+ (server.tags["Env"] || "???")[0...12],
17
+ (server.tags["Meal"] || "???")[0...10],
18
+ (server.tags["Creator"] || "???")[0...10],
19
+ server.state,
20
+ server.public_ip_address,
21
+ server.private_ip_address,
22
+ server.flavor_id
23
+ end
24
+
25
+ def self.print_servers(servers)
26
+ print_server_table_header
27
+ sorted_servers = servers.sort_by { |s| [s.tags["Env"] || "?",
28
+ s.tags["Meal"] || "?"] }
29
+ sorted_servers.each do |server|
30
+ print_server(server) if server
31
+ end
32
+ puts
33
+ end
34
+
4
35
  module Servers
5
36
  NotFound = Class.new(StandardError)
6
37
 
7
38
  class << self
39
+ def clear
40
+ @instance = nil
41
+ end
42
+
8
43
  def instance
9
44
  reload if !@instance
10
45
  @instance
@@ -48,44 +83,67 @@ module Boucher
48
83
  end
49
84
 
50
85
  def in_env(env)
51
- Servers.cultivate(self.find_all {|s| s.tags["Env"] == env.to_s })
86
+ Servers.cultivate(self.find_all { |s| s.tags["Env"] == env.to_s })
52
87
  end
53
88
 
54
89
  def in_state(state)
55
- Servers.cultivate(self.find_all {|s| s.state == state.to_s })
90
+ if state[0] == "!"
91
+ state = state[1..-1]
92
+ Servers.cultivate(self.find_all { |s| s.state != state.to_s })
93
+ else
94
+ Servers.cultivate(self.find_all { |s| s.state == state.to_s })
95
+ end
56
96
  end
57
97
 
58
98
  def of_meal(meal)
59
- Servers.cultivate(self.find_all {|s| s.tags["Meal"] == meal.to_s })
99
+ Servers.cultivate(self.find_all { |s| s.tags["Meal"] == meal.to_s })
60
100
  end
61
101
 
62
- def self.start(server_id)
63
- Boucher.change_server_state(server_id, :start, "running")
102
+ def self.start(servers)
103
+ Boucher.change_servers_state(servers, :start, "running")
64
104
  end
65
105
 
66
- def self.stop(server_id)
67
- Boucher.change_server_state(server_id, :stop, "stopped")
106
+ def self.stop(servers)
107
+ Boucher.change_servers_state(servers, :stop, "stopped")
68
108
  end
69
109
 
70
- def self.terminate(server)
71
- volumes = server.volumes
72
- volumes_to_destroy = volumes.select {|v| !v.delete_on_termination}
73
-
74
- Boucher.change_server_state server.id, :destroy, "terminated"
110
+ def self.restart(servers)
111
+ Boucher.change_servers_state(servers, :stop, "stopped")
112
+ Boucher.change_servers_state(servers, :start, "running")
113
+ end
75
114
 
76
- volumes_to_destroy.each do |volume|
77
- volume.wait_for { volume.state == 'available' }
78
- puts "Destroying volume #{volume.id}..."
79
- Boucher::Volumes.destroy(volume)
80
- end
115
+ def self.terminate(servers)
116
+ Boucher.change_servers_state(servers, :destroy, "terminated")
81
117
  end
82
118
 
83
119
  def with_id(server_id)
84
- Servers.cultivate(self.find_all {|s| s.id == server_id}).first
120
+ Servers.cultivate(self.find_all { |s| s.id == server_id }).first
85
121
  end
86
122
 
87
123
  def [](meal)
88
124
  find(:env => Boucher::Config[:env], :meal => meal, :state => "running")
89
125
  end
90
126
  end
127
+
128
+ def self.change_servers_state(servers, command, new_state)
129
+ print "#{command}-ing servers #{servers.map(&:id).join(", ")}..."
130
+ servers.each { |s| s.send(command.to_sym) }
131
+ servers.each { |s| s.wait_for { print "."; s.state == new_state }}
132
+ puts
133
+ Boucher.print_servers servers
134
+ puts
135
+ puts "The servers have been #{command}-ed."
136
+ end
137
+
138
+ def self.resolve_servers(id_or_meal)
139
+ if id_or_meal[0..1] == "i-"
140
+ puts "Retrieving server with id #{id_or_meal}..."
141
+ [Boucher::Servers.with_id(id_or_meal)]
142
+ else
143
+ puts "Searching for running #{id_or_meal} servers in #{Boucher.env_name} environment..."
144
+ servers = Boucher::Servers.search(:meal => id_or_meal, :env => Boucher.env_name, :state => "!terminated")
145
+ Boucher::print_servers(servers)
146
+ servers
147
+ end
148
+ end
91
149
  end
@@ -15,11 +15,35 @@ module Boucher
15
15
  @store
16
16
  end
17
17
 
18
+ FILE_TABLE_FORMAT = "%-60s %-10s %-25s %-32s\n"
19
+
20
+ def self.print_file_table_header
21
+ puts
22
+ printf FILE_TABLE_FORMAT, "Key", "Size", "Last Modified", "etag"
23
+ puts ("-" * 150)
24
+ end
25
+
26
+ def self.print_file(file)
27
+ printf FILE_TABLE_FORMAT,
28
+ file.key,
29
+ file.content_length,
30
+ file.last_modified,
31
+ file.etag
32
+ end
33
+
34
+ def self.print_files(files)
35
+ print_file_table_header
36
+ files.each do |file|
37
+ print_file(file) if file
38
+ end
39
+ puts
40
+ end
41
+
18
42
  module Storage
19
43
 
20
44
  def self.list(dir_name)
21
45
  dir = Boucher.storage.directories.get(dir_name)
22
- result = dir.files.select {|f| f.key[-1] != "/" }.to_a
46
+ result = dir.files.select { |f| f.key[-1] != "/" }.to_a
23
47
  result
24
48
  end
25
49
 
@@ -36,7 +60,7 @@ module Boucher
36
60
  dir = Boucher.storage.directories.get(dir_name)
37
61
  url = dir.files.get_https_url(key, Time.now + 3600)
38
62
  Kernel.system("curl", url, "-o", filename)
39
- dir.files.detect{|f| f.key == key}
63
+ dir.files.detect { |f| f.key == key }
40
64
  end
41
65
 
42
66
  end
@@ -0,0 +1,42 @@
1
+ require 'boucher/addresses'
2
+
3
+ namespace :addresses do
4
+
5
+ desc "Prints a list of allocated Elastic IP addresses"
6
+ task :list do
7
+ Boucher.print_address_overview(Boucher.address_overview)
8
+ end
9
+
10
+ desc "Allocates a new Elastic IP address"
11
+ task :allocate do
12
+ puts "Allocation a new Elastic IP address..."
13
+ address = Boucher.compute.addresses.create
14
+ Boucher.print_addresses([address])
15
+ end
16
+
17
+ desc "Releases an Elastic IP address"
18
+ task :deallocate, [:ip] do |t, args|
19
+ puts "Deallocating Elastic IP address: #{args.ip} ..."
20
+ address = Boucher.compute.addresses.get(args.ip)
21
+ raise "Elastic IP address not found: #{args.ip}" unless address
22
+ address.destroy
23
+ puts "Done."
24
+ end
25
+
26
+ desc "Associates an Elastic IP with a specific server"
27
+ task :associate, [:ip, :server_id] do |t, args|
28
+ server = Boucher.compute.servers.get(args.server_id)
29
+ raise "Server not found!" unless server
30
+ address = Boucher.compute.addresses.get(args.ip)
31
+ raise "Elastic IP not found!" unless address
32
+ address.server = server
33
+ Boucher.print_addresses [address]
34
+ end
35
+
36
+ desc "Associates all unbound Elastic IP addresses configured for all meals"
37
+ task :push do
38
+ Boucher.associate_all_addresses
39
+ end
40
+
41
+
42
+ end
@@ -44,17 +44,12 @@ namespace :servers do
44
44
  server_listing("of meal '#{args.meal}'", Boucher::Servers.of_meal(args.meal))
45
45
  end
46
46
 
47
- desc "Terminates the specified server"
48
- task :terminate, [:server_id] do |t, args|
49
- server = Boucher::Servers.with_id(args.server_id)
50
-
51
- if !server
52
- puts "Server #{args.server_id} does not exist"
53
- exit 1
54
- end
47
+ desc "Terminates the specified server(s)"
48
+ task :terminate, [:id_or_meal] do |t, args|
49
+ servers = Boucher.resolve_servers(args.id_or_meal)
55
50
 
56
51
  begin
57
- Boucher::Servers.terminate(server)
52
+ Boucher::Servers.terminate(servers) if !servers.empty?
58
53
  rescue => e
59
54
  puts "\nTermination failed. This may be due to termination protection. If
60
55
  you're sure you wish to disable this protection, select the instance in the AWS
@@ -63,16 +58,22 @@ web console and click Instance Actions -> Change Termination Protection -> Yes."
63
58
  end
64
59
  end
65
60
 
66
- desc "Stops the specified server"
67
- task :stop, [:server_id] do |t, args|
68
- server = Boucher.compute.servers.get(args.server_id)
69
- Boucher::Servers.stop(args.server_id)
61
+ desc "Stops the specified server(s)"
62
+ task :stop, [:id_or_meal] do |t, args|
63
+ servers = Boucher.resolve_servers(args.id_or_meal)
64
+ Boucher::Servers.stop(servers) if !servers.empty?
70
65
  end
71
66
 
72
- desc "Starts the specified server"
73
- task :start, [:server_id] do |t, args|
74
- Boucher::Servers.start(args.server_id)
75
- server = Boucher.compute.servers.get(args.server_id)
67
+ desc "Starts the specified server(s)"
68
+ task :start, [:id_or_meal] do |t, args|
69
+ servers = Boucher.resolve_servers(args.id_or_meal)
70
+ Boucher::Servers.start(servers) if !servers.empty?
71
+ end
72
+
73
+ desc "Restarts the specified server(s)"
74
+ task :restart, [:id_or_meal] do |t, args|
75
+ servers = Boucher.resolve_servers(args.id_or_meal)
76
+ Boucher::Servers.restart(servers) if !servers.empty?
76
77
  end
77
78
 
78
79
  desc "Open an SSH session with the specified server"
@@ -110,17 +111,10 @@ web console and click Instance Actions -> Change Termination Protection -> Yes."
110
111
  Boucher.establish_server(server, args.meal)
111
112
  end
112
113
 
113
- desc "Cook the specified meal on the instance specified, or deployed instances of that meal"
114
+ desc "Cook the specified meal on the instance(s) specified by the given id or meal"
114
115
  task :chef, [:meal, :server_id] do |t, args|
115
116
  Boucher.assert_env!
116
- servers = []
117
- if(args.server_id)
118
- servers = [Boucher.compute.servers.get(args.server_id)]
119
- else
120
- puts "Searching for running #{args.meal} servers in #{Boucher.env_name} environment..."
121
- servers = Boucher::Servers.search(:meal => args.meal, :env => Boucher.env_name, :state => "running")
122
- puts "Found #{servers.size}."
123
- end
117
+ servers = resolve_servers(args.server_id || args.meal)
124
118
  servers.each do |server|
125
119
  Boucher.cook_meal(server, args.meal)
126
120
  end
@@ -9,6 +9,12 @@ namespace :volumes do
9
9
 
10
10
  desc "Destroy the specified volume"
11
11
  task :destroy, [:volume_id] do |t, args|
12
- Boucher.destroy_volume(args.volume_id)
12
+ volume = Boucher::Volumes.with_id(args.volume_id)
13
+ if volume
14
+ puts "Destroying volume:"
15
+ print_volumes [volume]
16
+ else
17
+ raise "Volume not found: #{args.volume_id}"
18
+ end
13
19
  end
14
20
  end
@@ -1,9 +1,23 @@
1
1
  require 'boucher/compute'
2
2
 
3
3
  module Boucher
4
- def self.destroy_volume(volume_id)
5
- volume = Boucher::Volumes.with_id(volume_id)
6
- Volumes.destroy(volume)
4
+
5
+ VOLUME_TABLE_FORMAT = "%-12s %-15s %-6s %-10s %-10s %-13s\n"
6
+
7
+ def self.print_volumes(volumes)
8
+ puts
9
+ printf VOLUME_TABLE_FORMAT, "ID", "Name", "Size", "Server", "State", "Snapshot"
10
+ puts ("-" * 76)
11
+
12
+ volumes.each do |volume|
13
+ printf VOLUME_TABLE_FORMAT,
14
+ volume.id,
15
+ (volume.tags["Name"] || "")[0...15],
16
+ volume.size.to_s + "GB",
17
+ volume.server_id,
18
+ volume.state,
19
+ volume.snapshot_id
20
+ end
7
21
  end
8
22
 
9
23
  module Volumes
@@ -19,23 +33,25 @@ module Boucher
19
33
  end
20
34
 
21
35
  def self.with_id(volume_id)
22
- all.find {|volume| volume.id == volume_id}
36
+ all.find { |volume| volume.id == volume_id }
23
37
  end
24
38
 
25
- def self.create(zone, snapshot, device)
26
- response = Boucher.compute.create_volume(zone, snapshot.volume_size.to_i, snapshot.id)
39
+ def self.create(options)
40
+ zone = options[:availability_zone]
41
+ raise ":availability_zone is required to create a volume." unless zone
42
+ size = options[:size]
43
+ snapshot_id = options[:snapshot_id]
44
+ response = if snapshot_id
45
+ snapshot = Boucher::compute.snapshots.get(snapshot_id)
46
+ size = snapshot.volume_size.to_i
47
+ Boucher.compute.create_volume(zone, size, "SnapshotId" => snapshot_id)
48
+ else
49
+ Boucher.compute.create_volume(zone, size)
50
+ end
27
51
  volume_id = response.body["volumeId"]
28
- volume = Boucher.compute.volumes.get(volume_id)
29
-
30
- volume.wait_for { ready? }
31
- volume.device = device
52
+ volume = Boucher.compute.volumes.get(volume_id)
53
+ volume.wait_for { volume.ready? }
32
54
  volume
33
55
  end
34
-
35
- def self.attach(volumes, server)
36
- Array(volumes).each do |volume|
37
- Boucher.compute.attach_volume(server.id, volume.id, volume.device)
38
- end
39
- end
40
56
  end
41
57
  end
@@ -0,0 +1,73 @@
1
+ require_relative "../spec_helper"
2
+ require 'boucher/addresses'
3
+ require 'ostruct'
4
+
5
+ describe "Boucher Addresses" do
6
+
7
+ before do
8
+ Boucher::Servers.clear
9
+ end
10
+
11
+ after do
12
+ Boucher::Servers.all.each { |s| s.destroy }
13
+ Boucher::Config[:env] = "test"
14
+ end
15
+
16
+ it "does nothing for meal with no elastic ips" do
17
+ server = Boucher.compute.servers.create(tags: {"Meal" => "some_meal"})
18
+
19
+ Boucher.associate_addresses_for({name: "some_meal"}, server)
20
+
21
+ server.reload
22
+ server.addresses.should == []
23
+ end
24
+
25
+ it "associates ips for server" do
26
+ server = Boucher.compute.servers.create(tags: {"Meal" => "some_meal"})
27
+ ip = Boucher.compute.addresses.create
28
+
29
+ meal = {name: "some_meal", elastic_ips: [ip.public_ip]}
30
+ Boucher.associate_addresses_for(meal, server)
31
+
32
+ server.reload
33
+ server.addresses.count.should == 1
34
+ server.addresses.first.public_ip.should == ip.public_ip
35
+ end
36
+
37
+ it "associating ips skips missing ips" do
38
+ server = Boucher.compute.servers.create(tags: {"Meal" => "some_meal"})
39
+
40
+ meal = {name: "some_meal", elastic_ips: ["1.2.3.4"]}
41
+
42
+ lambda { Boucher.associate_addresses_for(meal, server) }.should_not raise_error
43
+ end
44
+
45
+ it "associates all ips for all meals" do
46
+ server1 = Boucher.compute.servers.create(tags: {"Meal" => "meal1", "Env" => "test"})
47
+ server2 = Boucher.compute.servers.create(tags: {"Meal" => "meal2", "Env" => "test"})
48
+ ip1 = Boucher.compute.addresses.create
49
+ ip2 = Boucher.compute.addresses.create
50
+
51
+ meals = {meal1: {name: "meal1", elastic_ips: [ip1.public_ip]},
52
+ meal2: {name: "meal2", elastic_ips: [ip2.public_ip]}}
53
+ Boucher.stub(:meals).and_return meals
54
+
55
+ Boucher.associate_all_addresses
56
+
57
+ server1.reload
58
+ server1.addresses.size.should == 1
59
+ server1.addresses.first.public_ip.should == ip1.public_ip
60
+ server2.reload
61
+ server2.addresses.first.public_ip.should == ip2.public_ip
62
+ end
63
+
64
+ it "associate all with missing server doesn't crash" do
65
+ ip = Boucher.compute.addresses.create
66
+
67
+ Boucher.meals[:some_meal] = {name: "some_meal", elastic_ips: [ip.public_ip]}
68
+
69
+ lambda { Boucher.associate_all_addresses }.should_not raise_error
70
+ end
71
+
72
+
73
+ end
@@ -4,7 +4,7 @@ require 'boucher/provision'
4
4
  describe "Boucher Provisioning" do
5
5
 
6
6
  before do
7
- Boucher::Config[:elastic_ips] = nil
7
+ Boucher::Servers.clear
8
8
  end
9
9
 
10
10
  after do
@@ -23,23 +23,13 @@ describe "Boucher Provisioning" do
23
23
  server = mock(:id => "the id", :state => "stopped")
24
24
  meal = {:name => "some_meal"}
25
25
  Boucher.stub(:meal).and_return(meal)
26
- Boucher.should_receive(:change_server_state).with("the id", :start, "running")
26
+ Boucher.should_receive(:change_servers_state).with([server], :start, "running")
27
27
  server.should_receive(:reload)
28
28
  Boucher.should_receive(:cook_meal_on_server).with(meal, server)
29
29
 
30
30
  Boucher.establish_server server, "some_meal"
31
31
  end
32
32
 
33
- it "attaches elastic IPs if the server was stopped" do
34
- server = mock(:id => "the id", :state => "stopped", :reload => nil)
35
- Boucher.stub(:meal).and_return({:name => "some_meal", :elastic_ips => %w(1.2.3.4)})
36
- Boucher.stub(:change_server_state)
37
- Boucher.stub(:cook_meal)
38
- Boucher.compute.should_receive(:associate_address).with(anything, "1.2.3.4")
39
-
40
- Boucher.establish_server server, "meal_name"
41
- end
42
-
43
33
  it "cooks meals on server if it is up and running" do
44
34
  running_server = mock(:id => "the id", :state => "running")
45
35
  meal = {:name => "some_meal"}
@@ -60,12 +50,73 @@ describe "Boucher Provisioning" do
60
50
  end
61
51
 
62
52
  it "provisions a server with elastic IP" do
53
+ Boucher.compute.key_pairs.create(name: "test_key")
54
+ ip = Boucher.compute.addresses.create
55
+ Boucher.stub!(:ssh)
56
+ Boucher.stub!(:cook_meal)
57
+
58
+ Boucher.provision :name => "some_meal", :elastic_ips => [ip.public_ip]
59
+
60
+ server = Boucher::Servers["some_meal"]
61
+ server.reload
62
+ server.addresses.size.should == 1
63
+ server.addresses.first.public_ip.should == ip.public_ip
64
+ end
65
+
66
+ it "attaches volumes" do
63
67
  Boucher.stub!(:ssh)
64
68
  Boucher.should_receive(:setup_meal)
65
- Boucher.stub(:cook_meal)
66
- Boucher.compute.should_receive(:associate_address).with(anything, "1.2.3.4")
69
+ Boucher.stub(:cook_meals_on_server)
70
+
71
+ Boucher.should_receive(:attach_volumes)
72
+
73
+ Boucher.provision :name => "some_meal", :volumes => {}
74
+ end
75
+ end
67
76
 
68
- Boucher.provision :name => "some_meal", :elastic_ips => %w(1.2.3.4)
77
+ context "Volumes" do
78
+
79
+ let(:server) { server = Boucher.compute.servers.new; server.save; server }
80
+
81
+ it "attaches an existing volume" do
82
+ volume = Boucher::Volumes.create(:size => 12, :availability_zone => "us-east-1c")
83
+
84
+ meal_spec = {:volumes => {"/dev/sda2" => {:volume_id => volume.id}}}
85
+ Boucher.attach_volumes(meal_spec, server)
86
+
87
+ server.reload
88
+ server.volumes.size.should == 1
89
+ server.volumes.first.device.should == "/dev/sda2"
90
+ server.volumes.first.availability_zone.should == "us-east-1c"
91
+ server.volumes.first.size.should == 12
92
+ end
93
+
94
+ it "attaches a new volume based on a snapshot" do
95
+ old_volume = Boucher::Volumes.create(:size => 12, :availability_zone => "us-east-1c")
96
+ response = old_volume.snapshot("test")
97
+ snapshot_id = response.body["snapshotId"]
98
+
99
+ meal_spec = {:volumes => {"/dev/sda3" => {:snapshot_id => snapshot_id}}}
100
+ Boucher.attach_volumes(meal_spec, server)
101
+
102
+ server.reload
103
+ server.volumes.size.should == 1
104
+ volume = server.volumes.first
105
+ volume.snapshot_id.should == snapshot_id
106
+ volume.size.should == 12
107
+ volume.device.should == "/dev/sda3"
108
+ end
109
+
110
+ it "attaches a new volume with specified size" do
111
+ meal_spec = {:volumes => {"/dev/sda4" => {:size => 42}}}
112
+ Boucher.attach_volumes(meal_spec, server)
113
+
114
+ server.reload
115
+ server.volumes.size.should == 1
116
+ volume = server.volumes.first
117
+ volume.size.should == 42
118
+ volume.device.should == "/dev/sda4"
69
119
  end
120
+
70
121
  end
71
122
  end
@@ -44,6 +44,13 @@ describe "Boucher::Servers" do
44
44
  Boucher::Servers.in_state("stopped").map(&:id).should == ["s1"]
45
45
  end
46
46
 
47
+ it "finds servers NOT in a given state" do
48
+ Boucher::Servers.in_state("!running").map(&:id).should == %w(s1 s2 s3)
49
+ Boucher::Servers.in_state("!terminated").map(&:id).should == %w(s1 s2 s4)
50
+ Boucher::Servers.in_state("!pending").map(&:id).should == %w(s1 s3 s4)
51
+ Boucher::Servers.in_state("!stopped").map(&:id).should == %w(s2 s3 s4)
52
+ end
53
+
47
54
  it "finds the first matching server" do
48
55
  Boucher::Servers.find.id.should == "s1"
49
56
  Boucher::Servers.find(:meal => "foo").id.should == "s1"
@@ -73,7 +80,8 @@ describe "Boucher::Servers" do
73
80
  end
74
81
 
75
82
  it "stops a server" do
76
- Boucher.should_receive(:change_server_state).with("the id", :stop, "stopped")
77
- Boucher::Servers.stop("the id")
83
+ server = OpenStruct.new(:id => "the id")
84
+ Boucher.should_receive(:change_servers_state).with([server], :stop, "stopped")
85
+ Boucher::Servers.stop([server])
78
86
  end
79
87
  end
@@ -0,0 +1,6 @@
1
+ require_relative "../spec_helper"
2
+ require 'boucher/util'
3
+
4
+ describe "Boucher Util" do
5
+
6
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "../spec_helper"
2
+ require 'boucher/volumes'
3
+ require 'ostruct'
4
+
5
+ describe "Boucher::Volumes" do
6
+
7
+ context "with mocked volumes" do
8
+ let(:remote_volumes) {
9
+ [OpenStruct.new(:id => "v1", :tags => {"Name" => "1", "Meal" => "foo"}, :size => 8),
10
+ OpenStruct.new(:id => "v2", :tags => {"Name" => "2", "Meal" => "bar"}, :size => 16),
11
+ OpenStruct.new(:id => "v3", :tags => {"Name" => "3", "Meal" => "foo"}, :size => 32)]
12
+ }
13
+
14
+ before do
15
+ Boucher.compute.stub(:volumes).and_return(remote_volumes)
16
+ end
17
+
18
+ after do
19
+ Boucher::Config[:env] = "test"
20
+ end
21
+
22
+ it "finds all volumes" do
23
+ Boucher::Volumes.all.size.should == 3
24
+ Boucher::Volumes.all.should == remote_volumes
25
+ end
26
+
27
+ it "finds volumes by id" do
28
+ Boucher::Volumes.with_id("v1").should == remote_volumes[0]
29
+ Boucher::Volumes.with_id("v2").should == remote_volumes[1]
30
+ Boucher::Volumes.with_id("v3").should == remote_volumes[2]
31
+ end
32
+ end
33
+
34
+ it "creates a volume" do
35
+ volume = Boucher::Volumes.create(:size => 12, :availability_zone => "us-east-1c")
36
+
37
+ volume.availability_zone.should == "us-east-1c"
38
+ volume.size.should == 12
39
+ end
40
+
41
+ it "creates a volume with snapshot" do
42
+ volume = Boucher::Volumes.create(:size => 12, :availability_zone => "us-east-1c")
43
+ response = volume.snapshot("test")
44
+
45
+ new_volume = Boucher::Volumes.create(:snapshot_id => response.body["snapshotId"], :availability_zone => "us-east-1c")
46
+
47
+ new_volume.size.should == 12
48
+ new_volume.availability_zone.should == "us-east-1c"
49
+ end
50
+ end
data/spec/spec_helper.rb CHANGED
@@ -19,18 +19,19 @@ Boucher::IO.mock!
19
19
 
20
20
 
21
21
  # MDM - Monkey patch wait_for methods so the tests are FASTER!
22
- module Fog
23
- def self.wait_for(timeout=Fog.timeout, interval=1)
24
- yield
25
- end
26
- end
27
-
28
- require 'fog/core/model'
22
+ # Unfortunately, Fog mocks depends on real time delays :.-(
23
+ #module Fog
24
+ # def self.wait_for(timeout=Fog.timeout, interval=1)
25
+ # yield
26
+ # end
27
+ #end
29
28
 
30
- module Fog
31
- class Model
32
- def wait_for(timeout=Fog.timeout, interval=1, &block)
33
- yield
34
- end
35
- end
36
- end
29
+ #require 'fog/core/model'
30
+ #
31
+ #module Fog
32
+ # class Model
33
+ # def wait_for(timeout=Fog.timeout, interval=1, &block)
34
+ # yield
35
+ # end
36
+ # end
37
+ #end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boucher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-27 00:00:00.000000000 Z
12
+ date: 2012-10-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -90,7 +90,9 @@ files:
90
90
  - LICENSE
91
91
  - README.md
92
92
  - Rakefile
93
+ - TODO.md
93
94
  - boucher.gemspec
95
+ - lib/boucher/addresses.rb
94
96
  - lib/boucher/compute.rb
95
97
  - lib/boucher/env.rb
96
98
  - lib/boucher/io.rb
@@ -99,12 +101,14 @@ files:
99
101
  - lib/boucher/servers.rb
100
102
  - lib/boucher/storage.rb
101
103
  - lib/boucher/tasks.rb
104
+ - lib/boucher/tasks/addresses.rake
102
105
  - lib/boucher/tasks/console.rake
103
106
  - lib/boucher/tasks/servers.rake
104
107
  - lib/boucher/tasks/storage.rake
105
108
  - lib/boucher/tasks/volumes.rake
106
109
  - lib/boucher/util.rb
107
110
  - lib/boucher/volumes.rb
111
+ - spec/boucher/addresses_spec.rb
108
112
  - spec/boucher/compute_spec.rb
109
113
  - spec/boucher/env_spec.rb
110
114
  - spec/boucher/io_spec.rb
@@ -112,6 +116,8 @@ files:
112
116
  - spec/boucher/provision_spec.rb
113
117
  - spec/boucher/servers_spec.rb
114
118
  - spec/boucher/storage_spec.rb
119
+ - spec/boucher/util_spec.rb
120
+ - spec/boucher/volumes_spec.rb
115
121
  - spec/spec_helper.rb
116
122
  homepage: http://github.com/8thlight/boucher
117
123
  licenses: []
@@ -125,9 +131,6 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
131
  - - ! '>='
126
132
  - !ruby/object:Gem::Version
127
133
  version: '0'
128
- segments:
129
- - 0
130
- hash: -3841890267981667096
131
134
  required_rubygems_version: !ruby/object:Gem::Requirement
132
135
  none: false
133
136
  requirements:
@@ -141,6 +144,7 @@ signing_key:
141
144
  specification_version: 3
142
145
  summary: AWS system deployment and management
143
146
  test_files:
147
+ - spec/boucher/addresses_spec.rb
144
148
  - spec/boucher/compute_spec.rb
145
149
  - spec/boucher/env_spec.rb
146
150
  - spec/boucher/io_spec.rb
@@ -148,4 +152,6 @@ test_files:
148
152
  - spec/boucher/provision_spec.rb
149
153
  - spec/boucher/servers_spec.rb
150
154
  - spec/boucher/storage_spec.rb
155
+ - spec/boucher/util_spec.rb
156
+ - spec/boucher/volumes_spec.rb
151
157
  - spec/spec_helper.rb