annex 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.rdoc +40 -0
- data/Rakefile +1 -0
- data/annex.gemspec +23 -0
- data/bin/annex +47 -0
- data/config/settings.yml.example +61 -0
- data/lib/annex.rb +9 -0
- data/lib/annex/cli.rb +119 -0
- data/lib/annex/command.rb +46 -0
- data/lib/annex/environment.rb +83 -0
- data/lib/annex/errors.rb +6 -0
- data/lib/annex/list.rb +20 -0
- data/lib/annex/provision.rb +237 -0
- data/lib/annex/version.rb +3 -0
- data/lib/mixins/fog.rb +69 -0
- data/templates/bootstrap.sh.erb +24 -0
- data/templates/ruby-1.9.3.sh.erb +5 -0
- data/templates/ruby-apt.sh.erb +5 -0
- data/templates/ruby-ree.sh.erb +2 -0
- data/templates/update.sh.erb +10 -0
- metadata +115 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
== Annex
|
2
|
+
|
3
|
+
Annex leverages chef-solo to allow you to provision and update multiple
|
4
|
+
servers by looking up network topology on the fly utilizing a distributed
|
5
|
+
repository to manage recipes.
|
6
|
+
|
7
|
+
== Getting started
|
8
|
+
|
9
|
+
The `annex` command allows you to provision a server (bootstrapping or
|
10
|
+
updating as needed) or list the servers that you have already provisioned.
|
11
|
+
|
12
|
+
Usage: annex [-v] [-h] command [<args>]
|
13
|
+
|
14
|
+
Available commands:
|
15
|
+
|
16
|
+
provision
|
17
|
+
list
|
18
|
+
|
19
|
+
Global options:
|
20
|
+
|
21
|
+
-h, --help Show this message
|
22
|
+
-v, --version Show version
|
23
|
+
|
24
|
+
In order for this to work, your recipes have to be setup and your
|
25
|
+
config/settings.yml needs to be setup.
|
26
|
+
|
27
|
+
== Testing
|
28
|
+
|
29
|
+
Nope.
|
30
|
+
|
31
|
+
== Contributors
|
32
|
+
|
33
|
+
* Jeff Rafter
|
34
|
+
* Your name here
|
35
|
+
|
36
|
+
== Acknowledgements
|
37
|
+
|
38
|
+
Thanks to Mitchell Hashimoto ({@mitchellh}[link:https://twitter.com/#!/mitchellh]) and
|
39
|
+
Nick Plante ({@zapnap}[link:https://twitter.com/#!/zapnap]).
|
40
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/annex.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "annex/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "annex"
|
7
|
+
s.version = Annex::VERSION
|
8
|
+
s.authors = ["Jeff Rafter"]
|
9
|
+
s.email = ["jeffrafter@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/jeffrafter/annex"
|
11
|
+
s.summary = %q{Quickly provision servers using chef-solo and no central server.}
|
12
|
+
s.description = %q{Annex leverages chef-solo to allow you to provision and update mutliple servers by looking up network topology on the fly utilizing a distributed repository to manage recipes"}
|
13
|
+
|
14
|
+
s.rubyforge_project = "annex"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency("fog", [">= 0"])
|
22
|
+
s.add_runtime_dependency("json", [">= 0"])
|
23
|
+
end
|
data/bin/annex
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Trap interrupts to quit cleanly.
|
4
|
+
Signal.trap("INT") { exit 1 }
|
5
|
+
|
6
|
+
require 'annex'
|
7
|
+
require 'annex/cli'
|
8
|
+
|
9
|
+
# Stdout/stderr should not buffer output
|
10
|
+
$stdout.sync = true
|
11
|
+
$stderr.sync = true
|
12
|
+
|
13
|
+
# Initialization options
|
14
|
+
opts = {}
|
15
|
+
|
16
|
+
# Are we running on Windows?
|
17
|
+
opts[:windows] = RbConfig::CONFIG["host_os"].downcase =~ /(mingw|mswin)/
|
18
|
+
|
19
|
+
# Disable color if the proper argument was passed or if we're
|
20
|
+
# on Windows since the default Windows terminal doesn't support
|
21
|
+
# colors.
|
22
|
+
opts[:support_colors] = opts[:windows] && ENV.has_key?("ANSICON")
|
23
|
+
opts[:no_colors] = ARGV.include?("--no-color") || !$stdout.tty? || !opts[:supports_colors]
|
24
|
+
ARGV.delete("--no-color")
|
25
|
+
|
26
|
+
env = nil
|
27
|
+
begin
|
28
|
+
# Create the environment, which is the cwd of wherever the
|
29
|
+
# `annex` command was invoked from
|
30
|
+
env = Annex::Environment.new(opts)
|
31
|
+
|
32
|
+
# Execute the CLI interface, and exit with the proper error code
|
33
|
+
exit(env.cli(ARGV))
|
34
|
+
|
35
|
+
rescue Annex::Errors::AnnexError => e
|
36
|
+
if env
|
37
|
+
env.ui.error e.message, {:prefix => false} if e.message
|
38
|
+
else
|
39
|
+
$stderr.puts "Annex failed to initialize at a very early stage:\n\n"
|
40
|
+
$stderr.puts e.message
|
41
|
+
end
|
42
|
+
|
43
|
+
exit e.status_code if e.respond_to?(:status_code)
|
44
|
+
|
45
|
+
# An error occurred with no status code defined
|
46
|
+
exit 999
|
47
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
repository: YOUR_PRIVATE_REPO
|
2
|
+
users:
|
3
|
+
bootstrap: ubuntu
|
4
|
+
update: youruser
|
5
|
+
amazon:
|
6
|
+
access_key_id: AMAZON_KEY_HERE
|
7
|
+
secret_access_key: AMAZON_SECRET_HERE
|
8
|
+
images:
|
9
|
+
ebs:
|
10
|
+
image_id: ami-63be790a
|
11
|
+
flavor_id: m2.2xlarge
|
12
|
+
ec2:
|
13
|
+
image_id: ami-35de095c
|
14
|
+
flavor_id: m1.large
|
15
|
+
ec2_small_32:
|
16
|
+
image_id: ami-81b275e8
|
17
|
+
flavor_id: m1.small
|
18
|
+
ebs_micro:
|
19
|
+
image_id: ami-71dc0b18
|
20
|
+
flavor_id: t1.micro
|
21
|
+
windows:
|
22
|
+
image_id: ami-92ba43fb
|
23
|
+
flavor_id: m1.small
|
24
|
+
roles:
|
25
|
+
web:
|
26
|
+
image: ec2
|
27
|
+
count: 2
|
28
|
+
app:
|
29
|
+
image: ec2
|
30
|
+
count: 1
|
31
|
+
utility:
|
32
|
+
image: ec2
|
33
|
+
count: 1
|
34
|
+
redis:
|
35
|
+
image: ec2
|
36
|
+
count: 1
|
37
|
+
"redis-slave":
|
38
|
+
image: ec2
|
39
|
+
count: 1
|
40
|
+
ci:
|
41
|
+
ruby: package
|
42
|
+
image: ec2_small_32
|
43
|
+
count: 1
|
44
|
+
qa:
|
45
|
+
image: ec2
|
46
|
+
count: 1
|
47
|
+
dns:
|
48
|
+
ruby: package
|
49
|
+
image: ebs_micro
|
50
|
+
count: 1
|
51
|
+
windows:
|
52
|
+
image: windows
|
53
|
+
count: 1
|
54
|
+
staging:
|
55
|
+
image: ec2
|
56
|
+
count: 1
|
57
|
+
monitoring:
|
58
|
+
ruby: package
|
59
|
+
image: ec2_small_32
|
60
|
+
count: 1
|
61
|
+
|
data/lib/annex.rb
ADDED
data/lib/annex/cli.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Annex
|
4
|
+
# Manages the command line interface to Annex
|
5
|
+
class CLI
|
6
|
+
COMMANDS = %w(provision list)
|
7
|
+
|
8
|
+
def initialize(argv, env)
|
9
|
+
@argv = argv
|
10
|
+
@env = env
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
exit_code = 0
|
15
|
+
return exit_code unless options = parse(@argv.dup)
|
16
|
+
command = case options[:command]
|
17
|
+
when "provision"
|
18
|
+
Annex::Provision.new(@env, options)
|
19
|
+
when "list"
|
20
|
+
Annex::List.new(@env, options)
|
21
|
+
end
|
22
|
+
env.info("Executing #{options[:command]}", :command)
|
23
|
+
command.execute
|
24
|
+
exit_code
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def env
|
30
|
+
@env
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(argv)
|
34
|
+
# Global option parser
|
35
|
+
parser = OptionParser.new do |opts|
|
36
|
+
opts.banner = "Usage: annex [-v] [-h] command [<args>]"
|
37
|
+
opts.separator ""
|
38
|
+
opts.separator "Available commands: "
|
39
|
+
opts.separator ""
|
40
|
+
|
41
|
+
COMMANDS.each do |c| opts.separator " #{c}" end
|
42
|
+
|
43
|
+
opts.separator ""
|
44
|
+
opts.separator "Global options:"
|
45
|
+
opts.separator ""
|
46
|
+
|
47
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
48
|
+
env.info opts
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on_tail("-v", "--version", "Show version") do
|
53
|
+
env.info "annex version #{Annex::VERSION}"
|
54
|
+
return
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# If there were no options then we show the usage and exit
|
59
|
+
if argv.nil? || argv.length == 0
|
60
|
+
env.info parser
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
# Grab the first arg, and setup the command options hash
|
65
|
+
options = {:command => argv.shift}
|
66
|
+
|
67
|
+
# Verify the commands
|
68
|
+
parser.order!([options[:command]]) do |unknown|
|
69
|
+
next if COMMANDS.include?(unknown)
|
70
|
+
env.error "Unknown command #{unknown.inspect}"
|
71
|
+
env.info opts
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
75
|
+
# Create a command specific parser
|
76
|
+
parser = case options[:command]
|
77
|
+
when "provision"
|
78
|
+
OptionParser.new do |opts|
|
79
|
+
opts.banner = "Usage: annex provision <args>"
|
80
|
+
opts.on("-r ROLE", "--role ROLE", "Specify the role for the server you are provisioning") do |role|
|
81
|
+
options[:role] = role
|
82
|
+
end
|
83
|
+
opts.on("-e ENVIRONMENT", "--environment ENVIRONMENT", "Specify the environment for the server you are provisioning") do |environment|
|
84
|
+
options[:environment] = environment
|
85
|
+
end
|
86
|
+
end
|
87
|
+
when "list"
|
88
|
+
OptionParser.new do |opts|
|
89
|
+
opts.banner = "Usage: annex list <args>"
|
90
|
+
opts.on("-r ROLE", "--role ROLE", "List all servers matching the specified role") do |role|
|
91
|
+
options[:role] = role
|
92
|
+
end
|
93
|
+
opts.on("-e ENVIRONMENT", "--environment ENVIRONMENT", "List all servers in the specified environment") do |environment|
|
94
|
+
options[:environment] = environment
|
95
|
+
end
|
96
|
+
opts.on("-a", "--all", "List all servers in all environments") do
|
97
|
+
options[:all] = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# If there were no command options then we show the usage and exit
|
103
|
+
if argv.length == 0
|
104
|
+
env.info parser
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
# Verify the commands
|
109
|
+
parser.parse!(argv) do |unknown|
|
110
|
+
env.error "Unknown option for #{options[:command]} command: #{unknown.inspect}"
|
111
|
+
env.info opts
|
112
|
+
return
|
113
|
+
end
|
114
|
+
|
115
|
+
# Send back the parsed options
|
116
|
+
options
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'fog'
|
2
|
+
|
3
|
+
# Need some better logging
|
4
|
+
require 'mixins/fog'
|
5
|
+
|
6
|
+
module Annex
|
7
|
+
class Command
|
8
|
+
def initialize(env, options)
|
9
|
+
@env = env
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def template(name, template_binding=nil)
|
20
|
+
content = File.read(File.join(File.expand_path(File.dirname(__FILE__)),"..","..","templates","#{name}.erb")) rescue nil
|
21
|
+
erb = ERB.new(content)
|
22
|
+
erb.result(template_binding || binding)
|
23
|
+
end
|
24
|
+
|
25
|
+
def connection
|
26
|
+
@connection = Fog::Compute.new({
|
27
|
+
:provider => 'AWS',
|
28
|
+
:region => @env.config['amazon']['region'] || 'us-east-1',
|
29
|
+
:aws_access_key_id => @env.config['amazon']['access_key_id'],
|
30
|
+
:aws_secret_access_key => @env.config['amazon']['secret_access_key']
|
31
|
+
})
|
32
|
+
end
|
33
|
+
|
34
|
+
def servers
|
35
|
+
@servers ||= connection.servers.all
|
36
|
+
end
|
37
|
+
|
38
|
+
def role
|
39
|
+
@role ||= @options[:role]
|
40
|
+
end
|
41
|
+
|
42
|
+
def environment
|
43
|
+
@environment ||= @options[:environment]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Annex
|
5
|
+
# Represents an Annex environment. The basic annex environment contains
|
6
|
+
# a config/settings.yml file defining the server cluster.
|
7
|
+
class Environment
|
8
|
+
|
9
|
+
# The `cwd` that this environment represents
|
10
|
+
attr_reader :cwd
|
11
|
+
|
12
|
+
# Initializes a new environment with the given options. The options
|
13
|
+
# is a hash where the main available key is `cwd`, which defines the
|
14
|
+
# location of the environment. If `cwd` is nil, then it defaults
|
15
|
+
# to the `Dir.pwd` (which is the cwd of the executing process).
|
16
|
+
def initialize(opts=nil)
|
17
|
+
opts = {
|
18
|
+
:cwd => nil,
|
19
|
+
:windows => false,
|
20
|
+
:supports_colors => true,
|
21
|
+
:no_colors => false
|
22
|
+
}.merge(opts || {})
|
23
|
+
|
24
|
+
# Set the default working directory
|
25
|
+
opts[:cwd] ||= ENV["ANNEX_CWD"] if ENV.has_key?("ANNEX_CWD")
|
26
|
+
opts[:cwd] ||= Dir.pwd
|
27
|
+
opts[:cwd] = Pathname.new(opts[:cwd])
|
28
|
+
raise Errors::AnnexError.new("Unknown current working directory") if !opts[:cwd].directory?
|
29
|
+
|
30
|
+
# Set instance variables for all the configuration parameters.
|
31
|
+
@cwd = opts[:cwd]
|
32
|
+
@colorize = opts[:supports_colors] || !opts[:no_colors]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return a human-friendly string for pretty printed or inspected
|
36
|
+
# instances.
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
def inspect
|
40
|
+
"#<#{self.class}: #{@cwd}>"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Makes a call to the CLI with the given arguments as if they
|
44
|
+
# came from the real command line (sometimes they do!). An example:
|
45
|
+
#
|
46
|
+
# env.cli("provision", "--role", "app", "--environment", "staging")
|
47
|
+
#
|
48
|
+
def cli(*args)
|
49
|
+
CLI.new(args.flatten, self).execute
|
50
|
+
end
|
51
|
+
|
52
|
+
# The configuration object represented by this environment. This
|
53
|
+
# will trigger the environment to load if it hasn't loaded yet.
|
54
|
+
#
|
55
|
+
# @return [hash]
|
56
|
+
def config
|
57
|
+
@config ||= YAML::load_file(File.join(@cwd, "config", "settings.yml"))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Output a message, formatted if we support colors
|
61
|
+
def info(message, level=:default)
|
62
|
+
$stdout.puts colorize(message, level)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Output a message, formatted if we support colors
|
66
|
+
def error(message)
|
67
|
+
$stderr.puts colorize(message, :error)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def colorize(message, level=:info)
|
73
|
+
# Terminal colors
|
74
|
+
colors = {
|
75
|
+
:error => "\e[31m", # red
|
76
|
+
:info => "\e[32m", # green
|
77
|
+
:command => "\e[33m", # yellow
|
78
|
+
:notice => "\e[34m" # blue
|
79
|
+
}
|
80
|
+
@colorize ? "#{colors[level]}#{message}\e[0m" : message
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/annex/errors.rb
ADDED
data/lib/annex/list.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Annex
|
2
|
+
class List < Command
|
3
|
+
def execute
|
4
|
+
which = servers.select do |server|
|
5
|
+
name_tag = server.tags["Name"]
|
6
|
+
name_tag = name_tag.gsub(/-i-[0-9a-f]+$/, '') rescue ''
|
7
|
+
|
8
|
+
choose = server.state == "running"
|
9
|
+
choose = choose && name_tag =~ /^#{role}/ if role
|
10
|
+
choose = choose && name_tag =~ /#{environment}$/ if environment
|
11
|
+
choose
|
12
|
+
end
|
13
|
+
|
14
|
+
which.each do |server|
|
15
|
+
@env.info(server.tags["Name"], :info)
|
16
|
+
@env.info(" #{server.dns_name}\n Public: #{server.public_ip_address}\n Private: #{server.private_ip_address}\n", :notice)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'json'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module Annex
|
6
|
+
class Provision < Command
|
7
|
+
def execute
|
8
|
+
if !role && !environment
|
9
|
+
@env.info("Sorry, you must include the role and the environment when provisioning", :error)
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
# Try to determine the environment
|
15
|
+
write_environment
|
16
|
+
|
17
|
+
which = servers.select do |server|
|
18
|
+
name_tag = server.tags["Name"]
|
19
|
+
name_tag = name_tag.gsub(/-i-[0-9a-f]+$/, '') rescue ''
|
20
|
+
|
21
|
+
choose = server.state == "running"
|
22
|
+
choose = choose && name_tag =~ /^#{role}/ if role
|
23
|
+
choose = choose && name_tag =~ /#{environment}$/ if environment
|
24
|
+
choose
|
25
|
+
end
|
26
|
+
|
27
|
+
msg = "We have #{which.length} servers"
|
28
|
+
msg << " for the #{role} role"
|
29
|
+
msg << " in the environment #{environment}"
|
30
|
+
@env.info(msg, :info)
|
31
|
+
|
32
|
+
count = @env.config['roles'][role]['count']
|
33
|
+
@env.info("We should have #{count}", :info)
|
34
|
+
|
35
|
+
# Try to be graceful
|
36
|
+
Thread.abort_on_exception = false
|
37
|
+
threads = []
|
38
|
+
|
39
|
+
# For each server we do have, update
|
40
|
+
which.each do |server|
|
41
|
+
threads << update(server, @env.config['users']['update']) unless ENV['SKIP_UPDATE']
|
42
|
+
end
|
43
|
+
|
44
|
+
# For each server we need, bootstrap
|
45
|
+
(count - which.length).times do
|
46
|
+
image = @env.config['roles'][role]['image']
|
47
|
+
kind = @env.config['amazon']['images'][image]
|
48
|
+
threads << bootstrap(connection, kind['image_id'], kind['flavor_id'], kind['az_id'], @env.config['users']['bootstrap'])
|
49
|
+
end
|
50
|
+
|
51
|
+
threads.each do |thr|
|
52
|
+
thr.join unless ENV['SYNC']
|
53
|
+
end
|
54
|
+
|
55
|
+
ensure
|
56
|
+
cleanup_environment
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def ruby_script
|
63
|
+
if @env.config['roles'][role]['ruby'] == "package"
|
64
|
+
template("ruby-apt.sh")
|
65
|
+
elsif @env.config['roles'][role]['ruby'] == "1.9.3"
|
66
|
+
template("ruby-1.9.3.sh")
|
67
|
+
else
|
68
|
+
template("ruby-ree.sh")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Bootstrap the environment with chef to handle chef-roles
|
73
|
+
def bootstrap_script(options={})
|
74
|
+
template_binding = OpenStruct.new(options)
|
75
|
+
template("bootstrap.sh", template_binding.instance_eval { binding })
|
76
|
+
end
|
77
|
+
|
78
|
+
# Bootstrap the environment with chef to handle chef-roles
|
79
|
+
def update_script(options={})
|
80
|
+
template_binding = OpenStruct.new(options)
|
81
|
+
template("update.sh", template_binding.instance_eval { binding })
|
82
|
+
end
|
83
|
+
|
84
|
+
def bootstrap(connection, image_id, flavor_id, az_id, user)
|
85
|
+
thr = Thread.new(connection, image_id, flavor_id, az_id, user) do |_connection, _image_id, _flavor_id, _az_id, _user|
|
86
|
+
sleep 1
|
87
|
+
|
88
|
+
@env.info("Bootstrapping #{role} server...", :info)
|
89
|
+
|
90
|
+
begin
|
91
|
+
# Build the server from the base AMI
|
92
|
+
server = _connection.servers.bootstrap({
|
93
|
+
:private_key_path => '~/.ssh/id_rsa',
|
94
|
+
:public_key_path => '~/.ssh/id_rsa.pub',
|
95
|
+
:availability_zone => _az_id,
|
96
|
+
:username => _user,
|
97
|
+
:image_id => _image_id,
|
98
|
+
:flavor_id => _flavor_id
|
99
|
+
})
|
100
|
+
|
101
|
+
node_name = "#{role}-#{environment}-#{server.identity}"
|
102
|
+
|
103
|
+
# Add the tags
|
104
|
+
_connection.tags.create(
|
105
|
+
:resource_id => server.identity,
|
106
|
+
:key => 'Name',
|
107
|
+
:value => node_name)
|
108
|
+
rescue Exception => e
|
109
|
+
@env.error("Error for #{server.id} (#{e.message})\n\n#{e.backtrace}", :error)
|
110
|
+
return
|
111
|
+
end
|
112
|
+
|
113
|
+
scp_options = { :forward_agent => true }
|
114
|
+
scp = Fog::SCP.new(server.public_ip_address, server.username, scp_options)
|
115
|
+
scp.upload(@envfile.path.to_s, "#{environment}.json")
|
116
|
+
|
117
|
+
ssh_options = { :forward_agent => true }
|
118
|
+
ssh = Fog::SSH.new(server.public_ip_address, server.username, ssh_options)
|
119
|
+
|
120
|
+
begin
|
121
|
+
return if _image_id == "windows"
|
122
|
+
script = bootstrap_script({
|
123
|
+
:environment => environment,
|
124
|
+
:node_name => node_name,
|
125
|
+
:role => role,
|
126
|
+
:user => @env.config['users']['bootstrap'],
|
127
|
+
:ruby_script => ruby_script,
|
128
|
+
:repository => @env.config['repository']
|
129
|
+
})
|
130
|
+
script.split(/\n/).each do |cmd|
|
131
|
+
next if cmd == ''
|
132
|
+
@env.info("")
|
133
|
+
@env.info("Running command:", :info)
|
134
|
+
@env.info("")
|
135
|
+
@env.info(" #{cmd}", :command)
|
136
|
+
ssh.run(cmd)
|
137
|
+
end
|
138
|
+
rescue Exception => e
|
139
|
+
@env.error("Error for #{server.id} (#{e.message})\n\n#{e.backtrace}", :error)
|
140
|
+
end
|
141
|
+
|
142
|
+
begin
|
143
|
+
@env.info("")
|
144
|
+
@env.info("Done", :info)
|
145
|
+
@env.info(" #{server.dns_name}", :notice)
|
146
|
+
@env.info(" Public: #{server.public_ip_address}", :notice)
|
147
|
+
@env.info(" Private: #{server.private_ip_address}", :notice)
|
148
|
+
rescue
|
149
|
+
end
|
150
|
+
end
|
151
|
+
thr.join if ENV['SYNC']
|
152
|
+
thr
|
153
|
+
end
|
154
|
+
|
155
|
+
def update(server, user)
|
156
|
+
thr = Thread.new(server, user) do |_server, _user|
|
157
|
+
sleep 1
|
158
|
+
|
159
|
+
@env.info("Updating #{_server.public_ip_address} (#{_server.id})\n", :info)
|
160
|
+
|
161
|
+
scp_options = { :forward_agent => true }
|
162
|
+
scp = Fog::SCP.new(_server.public_ip_address, _user, scp_options)
|
163
|
+
scp.upload(@envfile.path.to_s, "#{environment}.json")
|
164
|
+
|
165
|
+
ssh_options = { :forward_agent => true }
|
166
|
+
ssh = Fog::SSH.new(_server.public_ip_address, _user, ssh_options)
|
167
|
+
|
168
|
+
node_name = "#{role}-#{environment}-#{_server.identity}"
|
169
|
+
|
170
|
+
begin
|
171
|
+
script = update_script({
|
172
|
+
:environment => environment,
|
173
|
+
:node_name => node_name,
|
174
|
+
:role => role,
|
175
|
+
:user => @env.config['users']['update']
|
176
|
+
})
|
177
|
+
script.split(/\n/).each do |cmd|
|
178
|
+
next if cmd == ''
|
179
|
+
@env.info("")
|
180
|
+
@env.info("Running command:", :info)
|
181
|
+
@env.info("")
|
182
|
+
@env.info(" #{cmd}", :command)
|
183
|
+
ssh.run(cmd)
|
184
|
+
end
|
185
|
+
rescue Exception => e
|
186
|
+
@env.error("Error for #{_server.id} (#{e.message})\n\n#{e.backtrace}", :error)
|
187
|
+
end
|
188
|
+
|
189
|
+
begin
|
190
|
+
@env.info("")
|
191
|
+
@env.info("Done", :info)
|
192
|
+
@env.info(" #{_server.dns_name}", :notice)
|
193
|
+
@env.info(" Public: #{_server.public_ip_address}", :notice)
|
194
|
+
@env.info(" Private: #{_server.private_ip_address}", :notice)
|
195
|
+
rescue
|
196
|
+
end
|
197
|
+
end
|
198
|
+
thr.join if ENV['SYNC']
|
199
|
+
thr
|
200
|
+
end
|
201
|
+
|
202
|
+
def write_environment
|
203
|
+
@nodes = []
|
204
|
+
servers.each do |server|
|
205
|
+
next unless server.state == "running"
|
206
|
+
next unless server.tags["Name"] && server.tags["Name"] != ''
|
207
|
+
|
208
|
+
role = server.tags["Name"].gsub(/-.*/, '')
|
209
|
+
env = server.tags["Name"].gsub(/^[^-]+-/, '').gsub(/-.*/, '')
|
210
|
+
next unless env == environment
|
211
|
+
|
212
|
+
@nodes << {
|
213
|
+
:name => server.tags["Name"],
|
214
|
+
:role => role,
|
215
|
+
:environment => env,
|
216
|
+
:public_fqdn => server.dns_name,
|
217
|
+
:public_ip => server.public_ip_address,
|
218
|
+
:private_ip => server.private_ip_address
|
219
|
+
}
|
220
|
+
end
|
221
|
+
@nodes
|
222
|
+
|
223
|
+
@envfile = Tempfile.new("annex-#{environment}.json")
|
224
|
+
@envfile.puts({:id => environment, :nodes => @nodes}.to_json)
|
225
|
+
@envfile.flush
|
226
|
+
@envfile
|
227
|
+
end
|
228
|
+
|
229
|
+
def cleanup_environment
|
230
|
+
return unless @envfile
|
231
|
+
@envfile.close
|
232
|
+
@envfile.unlink
|
233
|
+
@envfile = nil
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end
|
data/lib/mixins/fog.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Fog
|
2
|
+
module SSH
|
3
|
+
class Real
|
4
|
+
def run(commands)
|
5
|
+
commands = [*commands]
|
6
|
+
results = []
|
7
|
+
begin
|
8
|
+
Net::SSH.start(@address, @username, @options) do |ssh|
|
9
|
+
commands.each do |command|
|
10
|
+
result = Result.new(command)
|
11
|
+
ssh.open_channel do |ssh_channel|
|
12
|
+
ssh_channel.request_pty
|
13
|
+
ssh_channel.exec(command) do |channel, success|
|
14
|
+
unless success
|
15
|
+
raise "Could not execute command: #{command.inspect}"
|
16
|
+
end
|
17
|
+
|
18
|
+
channel.on_data do |ch, data|
|
19
|
+
result.stdout << handle_data(channel, data)
|
20
|
+
end
|
21
|
+
|
22
|
+
channel.on_extended_data do |ch, type, data|
|
23
|
+
next unless type == 1
|
24
|
+
result.stderr << handle_error(channel, data)
|
25
|
+
end
|
26
|
+
|
27
|
+
channel.on_request('exit-status') do |ch, data|
|
28
|
+
result.status = data.read_long
|
29
|
+
end
|
30
|
+
|
31
|
+
channel.on_request('exit-signal') do |ch, data|
|
32
|
+
result.status = 255
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
ssh.loop
|
37
|
+
results << result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue Net::SSH::HostKeyMismatch => exception
|
41
|
+
exception.remember_host!
|
42
|
+
sleep 0.2
|
43
|
+
retry
|
44
|
+
end
|
45
|
+
results
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_data(channel, data)
|
49
|
+
data
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_error(channel, error)
|
53
|
+
error
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Real
|
58
|
+
def handle_data(channel, data)
|
59
|
+
puts data
|
60
|
+
data
|
61
|
+
end
|
62
|
+
|
63
|
+
def handle_error(channel, error)
|
64
|
+
puts error.red
|
65
|
+
error
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
sudo apt-get update
|
2
|
+
sudo apt-get -y install ssh git-core wget curl build-essential clang bison openssl zlib1g libxslt1.1 libssl-dev libxslt1-dev libxml2 libffi-dev libyaml-dev libxslt-dev autoconf libc6-dev libreadline6-dev zlib1g-dev libcurl4-openssl-dev
|
3
|
+
|
4
|
+
<%= ruby_script %>
|
5
|
+
|
6
|
+
sudo gem install rake --no-ri --no-rdoc
|
7
|
+
sudo gem install slimgems --no-ri --no-rdoc
|
8
|
+
sudo gem install chef -v 10.20.0 --no-ri --no-rdoc
|
9
|
+
|
10
|
+
sudo mkdir -p /var/chef-solo
|
11
|
+
sudo chown <%= user %> /var/chef-solo
|
12
|
+
|
13
|
+
echo "github.com,207.97.227.239 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==" >> ~/.ssh/known_hosts
|
14
|
+
|
15
|
+
git clone <%= repository %> /var/chef-solo/cookbooks
|
16
|
+
cp <%= environment %>.json /var/chef-solo/cookbooks/data_bags/environments/
|
17
|
+
|
18
|
+
sudo mkdir -p /etc/chef
|
19
|
+
sudo chown <%= user %> /etc/chef
|
20
|
+
cp /var/chef-solo/cookbooks/solo.rb /etc/chef
|
21
|
+
echo 'node_name "<%= node_name %>"' >> /etc/chef/solo.rb
|
22
|
+
echo '{ "run_list": ["role[<%= role %>]"]}' > /etc/chef/dna.json
|
23
|
+
|
24
|
+
sudo chef-solo -c /etc/chef/solo.rb -j /etc/chef/dna.json
|
@@ -0,0 +1,10 @@
|
|
1
|
+
sudo chown -R <%= user %>:<%= user %> /var/chef-solo
|
2
|
+
sudo chown -R <%= user %>:<%= user %> /etc/chef
|
3
|
+
cd /var/chef-solo/cookbooks && git pull origin master
|
4
|
+
cp <%= environment %>.json /var/chef-solo/cookbooks/data_bags/environments
|
5
|
+
|
6
|
+
cp /var/chef-solo/cookbooks/solo.rb /etc/chef
|
7
|
+
echo 'node_name "<%= node_name %>"' >> /etc/chef/solo.rb
|
8
|
+
echo '{ "run_list": ["role[<%= role %>]"]}' > /etc/chef/dna.json
|
9
|
+
|
10
|
+
sudo chef-solo -c /etc/chef/solo.rb -j /etc/chef/dna.json
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: annex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jeff Rafter
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-06-28 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: fog
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: json
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
description: Annex leverages chef-solo to allow you to provision and update mutliple servers by looking up network topology on the fly utilizing a distributed repository to manage recipes"
|
50
|
+
email:
|
51
|
+
- jeffrafter@gmail.com
|
52
|
+
executables:
|
53
|
+
- annex
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- .gitignore
|
60
|
+
- Gemfile
|
61
|
+
- README.rdoc
|
62
|
+
- Rakefile
|
63
|
+
- annex.gemspec
|
64
|
+
- bin/annex
|
65
|
+
- config/settings.yml.example
|
66
|
+
- lib/annex.rb
|
67
|
+
- lib/annex/cli.rb
|
68
|
+
- lib/annex/command.rb
|
69
|
+
- lib/annex/environment.rb
|
70
|
+
- lib/annex/errors.rb
|
71
|
+
- lib/annex/list.rb
|
72
|
+
- lib/annex/provision.rb
|
73
|
+
- lib/annex/version.rb
|
74
|
+
- lib/mixins/fog.rb
|
75
|
+
- templates/bootstrap.sh.erb
|
76
|
+
- templates/ruby-1.9.3.sh.erb
|
77
|
+
- templates/ruby-apt.sh.erb
|
78
|
+
- templates/ruby-ree.sh.erb
|
79
|
+
- templates/update.sh.erb
|
80
|
+
has_rdoc: true
|
81
|
+
homepage: http://github.com/jeffrafter/annex
|
82
|
+
licenses: []
|
83
|
+
|
84
|
+
post_install_message:
|
85
|
+
rdoc_options: []
|
86
|
+
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 3
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 3
|
104
|
+
segments:
|
105
|
+
- 0
|
106
|
+
version: "0"
|
107
|
+
requirements: []
|
108
|
+
|
109
|
+
rubyforge_project: annex
|
110
|
+
rubygems_version: 1.6.2
|
111
|
+
signing_key:
|
112
|
+
specification_version: 3
|
113
|
+
summary: Quickly provision servers using chef-solo and no central server.
|
114
|
+
test_files: []
|
115
|
+
|