boucher 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+