boucher 0.1.8 → 0.2.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/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