boucher 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+ .idea
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "template"]
2
+ path = template
3
+ url = git@github.com:8thlight/boucher_template.git
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm use 1.9.3-p125@boucher --create
2
+
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ gem 'fog'
4
+ gem 'pry'
5
+ gem 'rake'
6
+ gem 'retryable'
7
+ gem 'rspec', "~>2.10"
data/Gemfile.lock ADDED
@@ -0,0 +1,53 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (3.1.3)
5
+ coderay (1.0.7)
6
+ diff-lcs (1.1.3)
7
+ excon (0.16.3)
8
+ fog (1.6.0)
9
+ builder
10
+ excon (~> 0.14)
11
+ formatador (~> 0.2.0)
12
+ mime-types
13
+ multi_json (~> 1.0)
14
+ net-scp (~> 1.0.4)
15
+ net-ssh (>= 2.1.3)
16
+ nokogiri (~> 1.5.0)
17
+ ruby-hmac
18
+ formatador (0.2.3)
19
+ jruby-pageant (1.1.1)
20
+ method_source (0.8)
21
+ mime-types (1.19)
22
+ multi_json (1.3.6)
23
+ net-scp (1.0.4)
24
+ net-ssh (>= 1.99.1)
25
+ net-ssh (2.6.0)
26
+ jruby-pageant (>= 1.1.1)
27
+ nokogiri (1.5.5)
28
+ pry (0.9.10)
29
+ coderay (~> 1.0.5)
30
+ method_source (~> 0.8)
31
+ slop (~> 3.3.1)
32
+ rake (0.9.2.2)
33
+ retryable (1.3.1)
34
+ rspec (2.11.0)
35
+ rspec-core (~> 2.11.0)
36
+ rspec-expectations (~> 2.11.0)
37
+ rspec-mocks (~> 2.11.0)
38
+ rspec-core (2.11.1)
39
+ rspec-expectations (2.11.3)
40
+ diff-lcs (~> 1.1.3)
41
+ rspec-mocks (2.11.3)
42
+ ruby-hmac (0.4.0)
43
+ slop (3.3.3)
44
+
45
+ PLATFORMS
46
+ ruby
47
+
48
+ DEPENDENCIES
49
+ fog
50
+ pry
51
+ rake
52
+ retryable
53
+ rspec (~> 2.10)
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ Copyright (c) 2012 8th Light, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ <img src="" alt="boucher logo" title="Boucher" align="right"/>
2
+ # Boucher
3
+
4
+ ...
5
+
6
+ ## Getting Started
7
+
8
+
9
+ ### Creating a base image
10
+
11
+ 1. Launch new instance: Ubuntu Server 12.04.1 LTS
12
+ * Create a new key saved in your infrastructure project
13
+ * Be sure to add a security group that opens port 22 for SSH
14
+
15
+ 2. Update config/env/shared.rb
16
+ * :aws_key_filename - name of the .pem file you just created and saved in the project root
17
+ * :aws_region - which AWS region did you use?
18
+ * :aws_access_key_id and aws_secret_access_key - available in the AWS Management Console under Security Credentials
19
+
20
+ 3. List servers
21
+
22
+ rake servers:list
23
+
24
+ 4. SSH into new server. (:username config must be 'ubunutu' at this point)
25
+
26
+ rake servers:ssh[<instance id>]
27
+
28
+ 5. Create new poweruser (unless you like 'unubutu' as your poweruser).
29
+
30
+ sudo adduser <username>
31
+ sudo adduser <username> sudo
32
+ sudo mkdir /home/<username>/.ssh
33
+ sudo cp .ssh/authorized_keys /home/<username>/.ssh/
34
+ sudo chown -R <username>:<username> /home/<username>/.ssh
35
+
36
+ 6. Logout. Update config :username. Log back in.
37
+
38
+ rake servers:ssh[<instance id>]
39
+
40
+ 7. Delete the ubuntu user.
41
+
42
+ deluser ubuntu
43
+
44
+ 8. Enable sudo without typing password
45
+
46
+ sudo visudo
47
+ # add the following line at the end of the file:
48
+ <username> ALL=(ALL) NOPASSWD: ALL
49
+
50
+ 9. Install required pacakges and gems.
51
+
52
+ sudo apt-get update
53
+ sudo apt-get install ruby1.9.1 ruby1.9.1-dev git gcc make libxml2-dev libxslt1-dev
54
+ sudo apt-get upgrade
55
+ sudo gem install bundler chef
56
+
57
+ 10. Checkout your infrstructure repo. (Yes. You should push your repo even in this early stage.)
58
+ If you use github, you'll have to generate ssh keys and add them to the github repo.
59
+
60
+ cd ~/.ssh
61
+ ssh-keygen -t rsa -C "your_email@youremail.com"
62
+ # Copy id_rsa.pub to your github user's ssh keys
63
+ cd ..
64
+ git clone git@github.com:<github account name>/<your infratructure project name>.git infrastructure
65
+
66
+ 10. Customize to your liking.
67
+
68
+ * install your preferred vim dot files
69
+ * etc...
70
+
71
+
72
+
73
+
74
+
75
+ ## Usage
76
+
77
+ ...
78
+
79
+ ## License
80
+
81
+ Copyright (c) 2012 8th Light, Inc.
82
+ MIT License
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run unit specs"
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.pattern = "./spec/**/*_spec.rb"
7
+ end
8
+
9
+ desc "Default: run the specs."
10
+ task :default => :spec
data/boucher.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "boucher"
5
+ s.version = "0.1"
6
+ s.authors = ["'Micah Micah'"]
7
+ s.email = ["'micah@8thlight.com'"]
8
+ s.homepage = "http://github.com/8thlight/boucher"
9
+ s.summary = "AWS system deployment and management"
10
+ s.description = "AWS system deployment and management"
11
+ s.files = `git ls-files`.split("\n")
12
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
13
+ s.executables = []
14
+ s.require_paths = ["lib"]
15
+ s.autorequire = 'boucher/tasks'
16
+
17
+ s.add_dependency('rake', '>= 0.9.2.2')
18
+ s.add_dependency('fog', '>= 1.6.0')
19
+ s.add_dependency('retryable', '>= 1.3.1')
20
+ s.add_dependency('pry', '>= 0.9.10')
21
+ end
data/lib/boucher/ci.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Boucher
2
+ module CI
3
+ def self.each_server(&block)
4
+ threads = []
5
+ Boucher.each_required_server do |server, server_class|
6
+ thread = Thread.new { block.yield(server, server_class) }
7
+ threads << thread
8
+ end
9
+ threads.each { |t| t.join }
10
+ end
11
+
12
+ def self.terminate_server(server_class)
13
+ server = Boucher.get_server(server_class, "ci", "running")
14
+ Boucher.change_server_state server.id, :destroy, "terminated" if server
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,39 @@
1
+ require 'boucher/env'
2
+ require 'boucher/util'
3
+ require 'json'
4
+
5
+ module Boucher
6
+
7
+ def self.json_to_class(json)
8
+ parser = JSON.parser.new(json, :symbolize_names => true)
9
+ config = parser.parse
10
+ config[:boucher]
11
+ end
12
+
13
+ def self.server_classes
14
+ if @server_classes.nil?
15
+ @server_classes = {}
16
+ Dir.glob(File.join("config", "*.json")).each do |file|
17
+ spec = json_to_class(IO.read(file))
18
+ class_name = File.basename(file).to_sym
19
+ @server_classes[class_name] = spec
20
+ end
21
+ end
22
+ @server_classes
23
+ end
24
+
25
+ def self.classify(server, class_spec)
26
+ server.image_id = class_spec[:base_image_id] || Boucher::Config[:base_image_id]
27
+ server.flavor_id = class_spec[:flavor_id] || Boucher::Config[:default_instance_flavor_id]
28
+ server.groups = class_spec[:groups] || Boucher::Config[:default_instance_groups]
29
+ server.key_name = class_spec[:key_name] || Boucher::Config[:aws_key_filename]
30
+ server.tags = {}
31
+ server.tags["Name"] = "#{class_spec[:class_name] || "base"} #{Time.new.strftime("%Y%m%d%H%M%S")}"
32
+ server.tags["Class"] = class_spec[:class_name] || "base"
33
+ server.tags["CreatedAt"] = Time.new.strftime("%Y%m%d%H%M%S")
34
+ server.tags["Creator"] = current_user
35
+ server.tags["Env"] = Boucher::Config[:env]
36
+ server.tags["Volumes"] = class_spec[:volumes]
37
+ end
38
+
39
+ end
@@ -0,0 +1,93 @@
1
+ require 'boucher/env'
2
+ require 'boucher/classes'
3
+ require 'boucher/io'
4
+ require 'fog'
5
+ require 'retryable'
6
+
7
+ module Boucher
8
+
9
+ EC2_CONFIG = {
10
+ :provider => 'AWS',
11
+ :aws_secret_access_key => Boucher::Config[:aws_secret_access_key],
12
+ :aws_access_key_id => Boucher::Config[:aws_access_key_id],
13
+ :region => Boucher::Config[:aws_region]
14
+ }
15
+
16
+ def self.compute
17
+ @compute ||= Fog::Compute.new(EC2_CONFIG)
18
+ @compute
19
+ end
20
+
21
+ def self.snapshots
22
+ @snapshots ||= compute.snapshots
23
+ end
24
+
25
+ def self.ssh(server, command=nil)
26
+ command_arg = nil
27
+ if command
28
+ command_arg = "\"#{command}\""
29
+ end
30
+
31
+ command = "#{ssh_command} #{Boucher::Config[:username]}@#{server.dns_name} #{command_arg}"
32
+ verbose command
33
+ Kernel.system command
34
+ raise "command failed with code #{$?.exitstatus}" unless $?.success?
35
+ end
36
+
37
+ def self.ssh_command
38
+ "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i #{Boucher::Config[:aws_key_filename]}.pem"
39
+ end
40
+
41
+ def self.download(server, remote_filepath, local_filepath)
42
+ command = ["rsync", "-azb", "-e", ssh_command, "--delete-after", "#{Boucher::Config[:username]}@#{server.dns_name}:#{remote_filepath}", local_filepath]
43
+ Kernel.system(*command)
44
+ raise "command failed with code #{$?.exitstatus}: #{command.inspect}" unless $?.success?
45
+ end
46
+
47
+ def self.rsync(server, from, to)
48
+ command = ["rsync", "-azb", "-e", ssh_command, "--delete-after",
49
+ from, "#{Boucher::Config[:username]}@#{server.dns_name}:#{to}"]
50
+ Kernel.system(*command)
51
+ raise "command failed with code #{$?.exitstatus}: #{command.inspect}" unless $?.success?
52
+ end
53
+
54
+ def self.update_recipes(server)
55
+ puts "Updating recipes on #{server.id}"
56
+ ssh server, "cd infrastructure && git checkout . && git clean -d -f && git pull && bundle"
57
+
58
+ %w[cookbooks config tasks].each do |folder|
59
+ rsync server, "#{folder}/", "infrastructure/#{folder}/"
60
+ end
61
+ end
62
+
63
+ def self.cook_meal(server, meal)
64
+ Boucher::Nagios.remove_host(server)
65
+
66
+ update_recipes(server)
67
+ ssh server, "cd infrastructure && sudo BUTCHER_ENV=#{Boucher::Config[:env]} BRANCH=#{Boucher::Config[:branch]} chef-solo -c config/solo.rb -j config/#{meal}.json"
68
+
69
+ Boucher::Nagios.add_host(server)
70
+ end
71
+
72
+ def self.ssh_open?(server)
73
+ ssh server, "echo 'SSH is open for business!'"
74
+ true
75
+ rescue Exception => e
76
+ false
77
+ end
78
+
79
+ def self.change_server_state(server_id, command, new_state)
80
+ print "#{command}-ing server #{server_id}..."
81
+ server = compute.servers.get(server_id)
82
+ server.send(command.to_sym)
83
+ server.wait_for { print "."; state == new_state }
84
+ puts
85
+ Boucher.print_servers [server]
86
+ puts
87
+ puts "The server has been #{command}-ed."
88
+ end
89
+
90
+ def self.find_servers
91
+ compute.servers
92
+ end
93
+ end
@@ -0,0 +1,46 @@
1
+ module Boucher
2
+
3
+ Config = {
4
+ :branch => ENV["BRANCH"] || "master"
5
+ } unless defined?(Boucher::Config)
6
+
7
+ def self.env_name
8
+ ENV["BUTCHER_ENV"] ? ENV["BUTCHER_ENV"] : :dev
9
+ end
10
+
11
+ env_dir = File.expand_path("config/env")
12
+ valid_envs = Dir[File.join(env_dir, "**", "*.rb")].map { |path| File.basename(path, ".rb") }
13
+ env_path = File.expand_path("#{env_name}.rb", env_dir)
14
+
15
+ unless defined?(Boucher::NO_LOAD_CONFIG)
16
+ unless File.exists?(env_path)
17
+ raise "Config file #{env_path} doesn't exist.\nYou need to change your BUTCHER_ENV environment variable to a valid environment name.\nValid environments: #{valid_envs.join(", ")}"
18
+ end
19
+
20
+ load env_path
21
+ end
22
+
23
+ def self.force_env!(name)
24
+ env = name.to_sym
25
+ Boucher::Config[:branch] = ENV["BRANCH"] || "master"
26
+ load File.expand_path(File.dirname(__FILE__) + "/../../config/env/shared.rb") unless defined?(Boucher::NO_LOAD_CONFIG)
27
+ file = File.expand_path(File.dirname(__FILE__) + "/../../config/env/#{env}.rb")
28
+ unless defined?(Boucher::NO_LOAD_CONFIG)
29
+ if File.exist?(file)
30
+ load file
31
+ else
32
+ require_relative "../../config/env/shared"
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.assert_env!
38
+ unless ENV['BUTCHER_ENV']
39
+ raise 'BUTCHER_ENV must be set before running this command'
40
+ end
41
+
42
+ unless ENV['BRANCH']
43
+ raise 'BRANCH must be set before running this command'
44
+ end
45
+ end
46
+ end
data/lib/boucher/io.rb ADDED
@@ -0,0 +1,108 @@
1
+ module Boucher
2
+
3
+ module IO
4
+
5
+ def self.mock!
6
+ $stdout = StringIO.new
7
+ $stderr = StringIO.new
8
+ $stdin = StringIO.new
9
+ end
10
+
11
+ def self.real!
12
+ $stdout = STDOUT
13
+ $stderr = STDERR
14
+ $stdin = STDIN
15
+ end
16
+
17
+ end
18
+
19
+ def self.verbose(*args)
20
+ if ENV["VERBOSE"] != "false"
21
+ puts *args
22
+ end
23
+ end
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", "Class", "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["Class"] || "???")[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["Class"] || "?"]}
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
+ end
@@ -0,0 +1,50 @@
1
+ require 'boucher/servers'
2
+ require 'boucher/compute'
3
+
4
+ module Boucher
5
+ module Nagios
6
+ def self.remove_host(host)
7
+ return if host.tags["Class"] == "nagios_server"
8
+
9
+ monitors_for(host.tags["Env"]).each do |monitor|
10
+ commands = [
11
+ "cd /home/#{Boucher::Config[:username]}/infrastructure",
12
+ "sudo rake nagios:remove_host[#{host.id}]",
13
+ "sudo /etc/init.d/nagios3 restart"
14
+ ]
15
+ Boucher.ssh(monitor, commands.join(" && "))
16
+ end
17
+ end
18
+
19
+ def self.add_host(host)
20
+ return if checks_for(host.tags["Class"]).empty?
21
+
22
+ monitors_for(host.tags["Env"]).each do |monitor|
23
+ commands = [
24
+ "cd /home/#{Boucher::Config[:username]}/infrastructure",
25
+ "sudo rake nagios:add_host[#{host.id},#{host.public_ip_address},#{host.tags["Class"]}]",
26
+ "sudo /etc/init.d/nagios3 restart"
27
+ ]
28
+
29
+ Boucher.ssh(monitor, commands.join(" && "))
30
+ end
31
+ end
32
+
33
+ def self.checks_for(server_class)
34
+ config_path = File.expand_path("../../../config/#{server_class}.json", __FILE__)
35
+ return [] unless File.exists? config_path
36
+ server_configuration = JSON.parse(File.read config_path)
37
+ server_configuration["nagios"] || []
38
+ end
39
+
40
+ private
41
+
42
+ def self.monitors_for(env)
43
+ Boucher::Servers.all.select do |monitor|
44
+ monitor.tags["Class"] == "nagios_server" &&
45
+ monitor.tags["Env"] == env &&
46
+ monitor.dns_name
47
+ end
48
+ end
49
+ end
50
+ end