annex 0.0.2
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/.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
|
+
|