boucher 0.1

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.
@@ -0,0 +1,121 @@
1
+ require 'boucher/compute'
2
+ require 'boucher/io'
3
+ require 'boucher/servers'
4
+ require 'boucher/volumes'
5
+ require 'boucher/nagios'
6
+ require 'retryable'
7
+
8
+ module Boucher
9
+ def self.each_required_server(&block)
10
+ Boucher::Config[:servers].each do |server_class|
11
+ server = find_server(server_class, Boucher::Config[:env])
12
+ block.yield(server, server_class)
13
+ end
14
+ end
15
+
16
+ def self.get_server(server_class, environment, state)
17
+ begin
18
+ Boucher::Servers.find(:env => environment.to_s, :class => server_class, :state => state)
19
+ rescue Boucher::Servers::NotFound
20
+ nil
21
+ end
22
+ end
23
+
24
+ def self.find_server(server_class, environment)
25
+ get_server(server_class, environment, "stopped") || get_server(server_class, environment, "running")
26
+ end
27
+
28
+ def self.establish_all_servers
29
+ Boucher.each_required_server do |server, server_class|
30
+ # Retries after 2, 4, 8, 16, 32, and 64 seconds
31
+ retryable(:tries => 6, :sleep => lambda { |n| 2**n }) do
32
+ # A RuntimeError will sometimes be thrown here, with a message of:
33
+ # "command failed with code 255"
34
+ Boucher.establish_server(server, server_class)
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.establish_server(server, server_class)
40
+ if server.nil?
41
+ Boucher.provision(server_class, Boucher.server_classes[server_class.to_sym])
42
+ elsif server.state == "stopped"
43
+ Boucher::Servers.start(server.id)
44
+ server.reload
45
+ Boucher.cook_meals_on_server(server_class, Boucher.server_classes[server_class.to_sym], server)
46
+ else
47
+ Boucher.cook_meals_on_server(server_class, Boucher.server_classes[server_class.to_sym], server)
48
+ end
49
+ end
50
+
51
+ def self.provision(class_name, class_map)
52
+ puts "Provisioning new #{class_name} server..."
53
+ server = create_classified_server(class_map)
54
+ wait_for_server_to_boot(server)
55
+ wait_for_server_to_accept_ssh(server)
56
+ volumes = create_volumes(class_map, server)
57
+ attach_volumes(volumes, server)
58
+ cook_meals_on_server(class_name, class_map, server)
59
+ puts "\nThe new #{class_name} server has been provisioned! id: #{server.id}"
60
+ end
61
+
62
+ def self.attach_elastic_ips(class_name, server)
63
+ puts "Attaching elastic IPs..."
64
+ return unless Boucher::Config[:elastic_ips] && Boucher::Config[:elastic_ips][class_name]
65
+
66
+ puts "Associating #{server.id} with #{Boucher::Config[:elastic_ips][class_name]}"
67
+ compute.associate_address(server.id, Boucher::Config[:elastic_ips][class_name])
68
+ end
69
+
70
+ private
71
+
72
+ def self.cook_meals_on_server(class_name, class_map, server)
73
+ return unless class_map[:meals]
74
+
75
+ class_map[:meals].each do |meal|
76
+ meal = meal.call if meal.is_a?(Proc)
77
+ Boucher.cook_meal(server, meal)
78
+ end
79
+
80
+ attach_elastic_ips(class_name, server)
81
+ end
82
+
83
+ def self.wait_for_server_to_accept_ssh(server)
84
+ puts "Waiting for server's SSH port to open..."
85
+ Fog.wait_for { ssh_open?(server) }
86
+ puts
87
+ end
88
+
89
+ def self.wait_for_server_to_boot(server)
90
+ print "Waiting for server to boot..."
91
+ server.wait_for { print "."; server.ready? }
92
+ puts
93
+ Boucher.print_servers([server])
94
+ end
95
+
96
+ def self.create_classified_server(class_map)
97
+ server = compute.servers.new(:tags => {})
98
+ Boucher.classify(server, class_map)
99
+ server.save
100
+ Boucher.print_servers([server])
101
+ server
102
+ end
103
+
104
+ def self.create_volumes(class_map, server)
105
+ Array(class_map[:volumes]).map do |volume_name|
106
+ attributes = Boucher.volume_configs[volume_name]
107
+ snapshot = snapshots.get(attributes[:snapshot])
108
+ puts "Creating volume from snapshot #{snapshot.id}..."
109
+ Boucher::Volumes.create(server.availability_zone, snapshot, attributes[:device])
110
+ end
111
+ end
112
+
113
+ def self.attach_volumes(volumes, server)
114
+ volumes.each do |volume|
115
+ print "Attaching volume #{volume.id} to #{server.id}..."
116
+ Boucher::Volumes.attach(volume, server)
117
+ volume.wait_for { print "."; state == "in-use" }
118
+ puts
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,92 @@
1
+ require 'boucher/compute'
2
+
3
+ module Boucher
4
+ module Servers
5
+ NotFound = Class.new(StandardError)
6
+
7
+ class << self
8
+ def instance
9
+ reload if !@instance
10
+ @instance
11
+ end
12
+
13
+ def reload
14
+ @instance = Boucher.compute.servers
15
+ @instance.each {} # Wake up you lazy list!
16
+ cultivate(@instance)
17
+ end
18
+
19
+ %w{all of_class in_env in_state search find [] with_id}.each do |m|
20
+ module_eval "def #{m}(*args); instance.#{m}(*args); end"
21
+ end
22
+
23
+ def cultivate(thing)
24
+ thing.extend(Boucher::Servers) if thing
25
+ thing
26
+ end
27
+ end
28
+
29
+ def all
30
+ self
31
+ end
32
+
33
+ def search(options={})
34
+ servers = self
35
+ servers = servers.of_class(options[:class]) if options[:class]
36
+ servers = servers.in_env(options[:env]) if options[:env]
37
+ servers = servers.in_state(options[:state]) if options[:state]
38
+ servers
39
+ end
40
+
41
+ def find(options={})
42
+ servers = search(options)
43
+ first = servers.first
44
+ if first.nil?
45
+ raise Boucher::Servers::NotFound.new("No server matches criteria: #{options.inspect}")
46
+ end
47
+ first
48
+ end
49
+
50
+ def in_env(env)
51
+ Servers.cultivate(self.find_all {|s| s.tags["Env"] == env.to_s })
52
+ end
53
+
54
+ def in_state(state)
55
+ Servers.cultivate(self.find_all {|s| s.state == state.to_s })
56
+ end
57
+
58
+ def of_class(klass)
59
+ Servers.cultivate(self.find_all {|s| s.tags["Class"] == klass.to_s })
60
+ end
61
+
62
+ def self.start(server_id)
63
+ Boucher.change_server_state(server_id, :start, "running")
64
+ end
65
+
66
+ def self.stop(server_id)
67
+ Boucher.change_server_state(server_id, :stop, "stopped")
68
+ end
69
+
70
+ def self.terminate(server)
71
+ Boucher::Nagios.remove_host(server)
72
+ volumes = server.volumes
73
+ volumes_to_destroy = volumes.select {|v| !v.delete_on_termination}
74
+
75
+ Boucher.change_server_state server.id, :destroy, "terminated"
76
+
77
+ volumes_to_destroy.each do |volume|
78
+ volume.wait_for { volume.state == 'available' }
79
+ puts "Destroying volume #{volume.id}..."
80
+ Boucher::Volumes.destroy(volume)
81
+ end
82
+ end
83
+
84
+ def with_id(server_id)
85
+ Servers.cultivate(self.find_all {|s| s.id == server_id}).first
86
+ end
87
+
88
+ def [](klass)
89
+ find(:env => Boucher::Config[:env], :class => klass, :state => "running")
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,43 @@
1
+ require 'boucher/env'
2
+ require 'boucher/classes'
3
+ require 'fog'
4
+
5
+ module Boucher
6
+
7
+ S3_CONFIG = {
8
+ :provider => 'AWS',
9
+ :aws_secret_access_key => Boucher::Config[:aws_secret_access_key],
10
+ :aws_access_key_id => Boucher::Config[:aws_access_key_id]
11
+ }
12
+
13
+ def self.storage
14
+ @store ||= Fog::Storage.new(S3_CONFIG)
15
+ @store
16
+ end
17
+
18
+ module Storage
19
+
20
+ def self.list(dir_name)
21
+ dir = Boucher.storage.directories.get(dir_name)
22
+ result = dir.files.select {|f| f.key[-1] != "/" }.to_a
23
+ result
24
+ end
25
+
26
+ def self.put(dir_name, key, filename)
27
+ dir = Boucher.storage.directories.get(dir_name)
28
+ body = File.open(filename)
29
+ file = dir.files.new(:key => key, :body => body)
30
+ file.save
31
+ body.close
32
+ file
33
+ end
34
+
35
+ def self.get(dir_name, key, filename)
36
+ dir = Boucher.storage.directories.get(dir_name)
37
+ url = dir.files.get_https_url(key, Time.now + 3600)
38
+ Kernel.system("curl", url, "-o", filename)
39
+ dir.files.detect{|f| f.key == key}
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ require 'boucher/compute'
2
+ require 'boucher/env'
3
+ require 'boucher/io'
4
+ require 'boucher/classes'
5
+ require 'boucher/provision'
6
+ require 'boucher/servers'
7
+ require 'boucher/volumes'
8
+ require 'boucher/nagios'
9
+
10
+ desc "Starts a console with the Boucher modules loaded"
11
+ task :console do
12
+ require 'pry'
13
+
14
+ Dir[File.expand_path("../../lib/boucher/**/*.rb", __FILE__)].each do |filename|
15
+ require filename
16
+ end
17
+
18
+ include Boucher
19
+
20
+ pry
21
+ end
@@ -0,0 +1,42 @@
1
+ require 'boucher/env'
2
+ require 'boucher/provision'
3
+ require 'retryable'
4
+
5
+ namespace :env do
6
+
7
+ desc "Attaches elastic IPs"
8
+ task :elastic_ips, [:env] do |t, args|
9
+ Boucher.force_env!(args.env)
10
+ Boucher.each_required_server do |server, server_class|
11
+ Boucher.attach_elastic_ips(server_class, server)
12
+ end
13
+ end
14
+
15
+ desc "Starts and deploys all the servers for the specified environment"
16
+ task :start, [:env] do |t, args|
17
+ Boucher.force_env!(args.env)
18
+ Boucher.assert_env!
19
+ Boucher.establish_all_servers
20
+ end
21
+
22
+ desc "Stops all the servers for the specified environment"
23
+ task :stop, [:env] do |t, args|
24
+ Boucher.force_env!(args.env)
25
+
26
+ Boucher.each_required_server do |server, server_class|
27
+ if server
28
+ Boucher::Servers.stop(server.id)
29
+ else
30
+ puts "No #{server_class} server found for #{Boucher::Config[:env]} environment."
31
+ end
32
+ end
33
+ end
34
+
35
+ desc "Terminates all servers for the specified environment"
36
+ task :terminate, [:env] do |t, args|
37
+ Boucher.force_env!(args.env)
38
+ Boucher.each_required_server do |server, server_class|
39
+ Boucher::Servers.terminate(server) if server
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,74 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ def make_config_path(host_name)
5
+ "/etc/nagios3/conf.d/#{host_name}.cfg"
6
+ end
7
+
8
+ def host_entry(host_name, ip)
9
+ <<-HOST
10
+ define host {
11
+ host_name #{host_name}
12
+ alias #{host_name}
13
+ address #{ip}
14
+ check_command check_ssh
15
+ notification_interval 0
16
+ notification_period 24x7
17
+ max_check_attempts 10
18
+ notification_options d,u,r
19
+ notifications_enabled 1
20
+ }
21
+ HOST
22
+ end
23
+
24
+ def service_entry(host_name, ip, plugin)
25
+ <<-SERVICE
26
+ define command {
27
+ command_name #{host_name}-#{plugin}
28
+ command_line /usr/lib/nagios/plugins/check_nrpe -H #{ip} -c "#{plugin}"
29
+ }
30
+
31
+ define service {
32
+ use generic-service
33
+ host_name #{host_name}
34
+ check_command #{host_name}-#{plugin}
35
+ service_description #{plugin}
36
+ normal_check_interval 5
37
+ check_period 24x7
38
+ notifications_enabled 1
39
+ }
40
+ SERVICE
41
+ end
42
+
43
+ namespace :nagios do
44
+ desc "Remove a nagios host from this machine"
45
+ task :remove_host, [:name] do |t, args|
46
+ FileUtils.rm_f make_config_path(args.name)
47
+ end
48
+
49
+ desc "Adds a nagios host to be monitored by this machine"
50
+ task :add_host, [:name, :ip, :klass] do |t, args|
51
+ checks = Boucher::Nagios.checks_for(args.klass)
52
+ return if checks.empty?
53
+
54
+ File.open(make_config_path(args.name), "w") do |file|
55
+ host_name = "#{args.klass}-#{args.name}"
56
+
57
+ file.puts host_entry(host_name, args.ip)
58
+
59
+ checks.each do |plugin, command|
60
+ file.puts
61
+ file.puts service_entry(host_name, args.ip, plugin)
62
+ end
63
+ end
64
+ end
65
+
66
+ desc "Opens the nagios web console"
67
+ task :open do
68
+ server = Boucher::Servers["nagios_server"]
69
+ url = "http://#{server.public_ip_address}/nagios3"
70
+ puts "Nagios lives at #{url}"
71
+ puts "Login using nagiosadmin / 'we are many lonely souls'"
72
+ `open #{url}`
73
+ end
74
+ end
@@ -0,0 +1,127 @@
1
+ require 'boucher/compute'
2
+ require 'boucher/env'
3
+ require 'boucher/io'
4
+ require 'boucher/classes'
5
+ require 'boucher/provision'
6
+ require 'boucher/servers'
7
+ require 'boucher/volumes'
8
+ require 'boucher/nagios'
9
+ require 'retryable'
10
+
11
+ def meals
12
+ config_files = Dir.glob("config/*.json")
13
+ configs = config_files.map { |config_file| File.basename(config_file) }
14
+ configs.map { |config| config.gsub(".json", "") }
15
+ end
16
+
17
+ def server_listing(description, servers)
18
+ puts "Listing all AWS server #{description}..."
19
+ Boucher.print_servers servers
20
+ puts
21
+ puts "#{servers.size} server(s)"
22
+ end
23
+
24
+ namespace :servers do
25
+ desc "List all volumes for a given server"
26
+ task :volumes, [:server_id] do |t, args|
27
+ volumes = Boucher::Servers.with_id(args.server_id).volumes
28
+ Boucher.print_volumes(volumes)
29
+ end
30
+
31
+ desc "List ALL AWS servers, with optional [env] param."
32
+ task :list, [:env] do |t, args|
33
+ servers = Boucher::Servers.all
34
+ servers = args.env ? servers.in_env(args.env) : servers
35
+ server_listing("", servers)
36
+ end
37
+
38
+ desc "List AWS servers in specified environment"
39
+ task :in_env, [:env] do |t, args|
40
+ server_listing("in the '#{args.env}' environment", Boucher::Servers.in_env(args.env))
41
+ end
42
+
43
+ desc "List AWS servers in specified class"
44
+ task :of_class, [:klass] do |t, args|
45
+ server_listing("of class '#{args.klass}'", Boucher::Servers.of_class(args.klass))
46
+ end
47
+
48
+ desc "Terminates the specified server"
49
+ task :terminate, [:server_id] do |t, args|
50
+ server = Boucher::Servers.with_id(args.server_id)
51
+
52
+ if !server
53
+ puts "Server #{args.server_id} does not exist"
54
+ exit 1
55
+ end
56
+
57
+ begin
58
+ Boucher::Servers.terminate(server)
59
+ rescue => e
60
+ puts "\nTermination failed. This may be due to termination protection. If
61
+ you're sure you wish to disable this protection, select the instance in the AWS
62
+ web console and click Instance Actions -> Change Termination Protection -> Yes."
63
+ raise
64
+ end
65
+ end
66
+
67
+ desc "Stops the specified server"
68
+ task :stop, [:server_id] do |t, args|
69
+ server = Boucher.compute.servers.get(args.server_id)
70
+ Boucher::Nagios.remove_host(server)
71
+ Boucher::Servers.stop(args.server_id)
72
+ end
73
+
74
+ desc "Starts the specified server"
75
+ task :start, [:server_id] do |t, args|
76
+ Boucher::Servers.start(args.server_id)
77
+ server = Boucher.compute.servers.get(args.server_id)
78
+ Boucher::Nagios.add_host(server)
79
+ end
80
+
81
+ desc "Open an SSH session with the specified server"
82
+ task :ssh, [:server_id] do |t, args|
83
+ puts "Opening SSH session to #{args.server_id}"
84
+ server = Boucher.compute.servers.get(args.server_id)
85
+ Boucher.ssh server
86
+ end
87
+
88
+ desc "Download a file from the server"
89
+ task :download, [:server_id, :filepath] do |t, args|
90
+ puts "Downloading #{args.filepath}"
91
+
92
+ server = Boucher.compute.servers.get(args.server_id)
93
+ remote_path = args.filepath
94
+ local_path = File.expand_path(File.join("..", "..", File.basename(args.filepath)), __FILE__)
95
+
96
+ Boucher.download(server, remote_path, local_path)
97
+ end
98
+
99
+ desc "Fix permissions on key files"
100
+ task :key_fix do
101
+ system "chmod 0600 *.pem"
102
+ end
103
+
104
+ desc "Provision new server [#{Boucher.server_classes.keys.sort.join(', ')}]"
105
+ task :provision, [:klass] do |t, args|
106
+ class_map = Boucher.server_classes[args.klass.to_sym]
107
+ Boucher.provision(args.klass, class_map)
108
+ end
109
+
110
+ desc "Provision new, or chef existing server of the specified class"
111
+ task :establish, [:klass] do |t, args|
112
+ Boucher.assert_env!
113
+ server = Boucher.find_server(args.klass, ENV['BUTCHER_ENV'])
114
+ Boucher.establish_server(server, args.klass)
115
+ end
116
+
117
+ namespace :chef do
118
+ meals.each do |s_class|
119
+ desc "Cook #{s_class} meal"
120
+ task s_class, [:server_id] do |t, args|
121
+ Boucher.assert_env!
122
+ server = Boucher.compute.servers.get(args.server_id)
123
+ Boucher.cook_meal(server, s_class)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,32 @@
1
+ require 'boucher/storage'
2
+ require 'boucher/io'
3
+
4
+ namespace :storage do
5
+
6
+ desc "Lists all the files in the infrastructure bucket on S3"
7
+ task :list do
8
+ files = Boucher::Storage.list("infrastructure")
9
+ Boucher.print_files files
10
+ end
11
+
12
+ desc "Puts a file in the infrastructure bucket"
13
+ task :put, [:file] do |t, args|
14
+ filename = args.file
15
+ key = File.basename(filename)
16
+ puts "Storing file #{filename} as #{key}"
17
+ file = Boucher::Storage.put("infrastructure", key, filename)
18
+ Boucher.print_files [file]
19
+ puts "File uploaded as #{key}."
20
+ end
21
+
22
+ desc "Gets a file from the infrastructure bucket. The file arg is the key on AWS."
23
+ task :get, [:file] do |t, args|
24
+ key = args.file
25
+ filename = File.basename(key)
26
+ puts "Getting file #{key} and saving to #{filename}"
27
+ file = Boucher::Storage.get("infrastructure", key, filename)
28
+ Boucher.print_files [file]
29
+ puts "File saved locally as #{filename}."
30
+ end
31
+
32
+ end
@@ -0,0 +1,14 @@
1
+ require 'boucher/volumes'
2
+ require 'boucher/io'
3
+
4
+ namespace :volumes do
5
+ desc "List provisioned volumes"
6
+ task :list do
7
+ Boucher.print_volumes(Boucher::Volumes.all)
8
+ end
9
+
10
+ desc "Destroy the specified volume"
11
+ task :destroy, [:volume_id] do |t, args|
12
+ Boucher.destroy_volume(args.volume_id)
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+
2
+ task_dir = File.expand_path(File.join(File.dirname(__FILE__), "tasks"))
3
+
4
+ Dir.glob(File.join(task_dir, "*.rake")).each do |lib|
5
+ load lib
6
+ end
@@ -0,0 +1,9 @@
1
+ module Boucher
2
+
3
+ def self.current_user
4
+ `git config user.name`.strip
5
+ rescue
6
+ "unknown"
7
+ end
8
+
9
+ end
@@ -0,0 +1,41 @@
1
+ require 'boucher/compute'
2
+
3
+ module Boucher
4
+ def self.destroy_volume(volume_id)
5
+ volume = Boucher::Volumes.with_id(volume_id)
6
+ Volumes.destroy(volume)
7
+ end
8
+
9
+ module Volumes
10
+ def self.all
11
+ @volumes ||= Boucher.compute.volumes
12
+ end
13
+
14
+ def self.destroy(volumes)
15
+ Array(volumes).each do |volume|
16
+ volume.reload
17
+ volume.destroy
18
+ end
19
+ end
20
+
21
+ def self.with_id(volume_id)
22
+ all.find {|volume| volume.id == volume_id}
23
+ end
24
+
25
+ def self.create(zone, snapshot, device)
26
+ response = Boucher.compute.create_volume(zone, snapshot.volume_size.to_i, snapshot.id)
27
+ volume_id = response.body["volumeId"]
28
+ volume = Boucher.compute.volumes.get(volume_id)
29
+
30
+ volume.wait_for { ready? }
31
+ volume.device = device
32
+ volume
33
+ 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
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ require_relative "../spec_helper"
2
+ require 'boucher/classes'
3
+ require 'ostruct'
4
+
5
+ describe "Boucher Server Classes" do
6
+
7
+ before do
8
+ Boucher.stub(:current_user).and_return("joe")
9
+ @server = OpenStruct.new
10
+ end
11
+
12
+ it "pull classification from json" do
13
+ json = "{\"boucher\": {\"foo\": 1,\n \"bar\": 2}}"
14
+ Boucher.json_to_class(json).should == {:foo => 1, :bar => 2}
15
+ end
16
+
17
+ it "can classify base server" do
18
+ some_class = {:class_name => "base",
19
+ :meals => ["base"]}
20
+ Boucher::Config[:default_instance_flavor_id] = 'm1.small'
21
+ Boucher::Config[:default_instance_groups] = ["SSH"]
22
+ Boucher.classify(@server, some_class)
23
+
24
+ @server.image_id.should == Boucher::Config[:base_image_id]
25
+ @server.flavor_id.should == 'm1.small'
26
+ @server.groups.should == ["SSH"]
27
+ @server.key_name.should == "test_key"
28
+ @server.tags["Class"].should == "base"
29
+ @server.tags["Name"].should_not == nil
30
+ @server.tags["Creator"].should == "joe"
31
+ @server.tags["Env"].should == Boucher::Config[:env]
32
+ end
33
+
34
+ end
35
+