hubcap 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Capfile.example +37 -0
- data/README.md +168 -0
- data/Rakefile +10 -0
- data/bin/hubcap +98 -0
- data/hubcap.gemspec +17 -0
- data/lib/hubcap/application.rb +24 -0
- data/lib/hubcap/group.rb +223 -0
- data/lib/hubcap/hub.rb +125 -0
- data/lib/hubcap/recipes/puppet.rb +176 -0
- data/lib/hubcap/recipes/servers.rb +21 -0
- data/lib/hubcap/server.rb +46 -0
- data/lib/hubcap/version.rb +5 -0
- data/lib/hubcap.rb +29 -0
- data/test/data/example.rb +68 -0
- data/test/data/parts/foo_param.rb +1 -0
- data/test/data/readme.rb +47 -0
- data/test/data/simple.rb +4 -0
- data/test/test_helper.rb +2 -0
- data/test/unit/test_application.rb +20 -0
- data/test/unit/test_group.rb +176 -0
- data/test/unit/test_hub.rb +104 -0
- data/test/unit/test_hubcap.rb +39 -0
- data/test/unit/test_server.rb +69 -0
- metadata +94 -0
data/Capfile.example
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# This just lets us test Hubcap without installing it as a gem...
|
2
|
+
#
|
3
|
+
$LOAD_PATH.unshift('lib')
|
4
|
+
|
5
|
+
# Force Hubcap into agnostic mode with AGNOSTIC=1, or application mode with 0.
|
6
|
+
# Use this environment variable with care!
|
7
|
+
#
|
8
|
+
# if ag = ENV['AGNOSTIC']
|
9
|
+
# set(:hubcap_agnostic, ag == '1') if ['0', '1'].include?(ag)
|
10
|
+
# end
|
11
|
+
|
12
|
+
|
13
|
+
# SSH user (and login password if required).
|
14
|
+
#
|
15
|
+
set(:user, ENV['AS'] || 'deploy')
|
16
|
+
set(:password) {
|
17
|
+
puts
|
18
|
+
warn("A server is prompting for the \"#{user}\" user's login password.")
|
19
|
+
warn("NB: You can run as another user with the AS environment variable.")
|
20
|
+
Capistrano::CLI.password_prompt
|
21
|
+
}
|
22
|
+
|
23
|
+
|
24
|
+
# This is only processed if we are using cap directly (not bin/hubcap).
|
25
|
+
#
|
26
|
+
# Load servers and sets from node config. Any recipes loaded after this
|
27
|
+
# point will be available only in application mode.
|
28
|
+
#
|
29
|
+
unless exists?(:hubcap)
|
30
|
+
if (target = ENV['TO']) && !ENV['TO'].empty?
|
31
|
+
target = '' if target == 'ALL'
|
32
|
+
require('hubcap')
|
33
|
+
Hubcap.load(target, 'test/data').configure_capistrano(self)
|
34
|
+
else
|
35
|
+
warn("NB: No servers specified. Target a Hubcap group or server with TO.")
|
36
|
+
end
|
37
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# Hubcap
|
2
|
+
|
3
|
+
Create a hub for your server configuration. Use it with Capistrano,
|
4
|
+
Puppet and others.
|
5
|
+
|
6
|
+
|
7
|
+
## Meet Hubcap
|
8
|
+
|
9
|
+
You want to provision your servers with Puppet. You want to deploy to your
|
10
|
+
servers with Capistrano. Where do you define your server infrastructure?
|
11
|
+
|
12
|
+
Hubcap lets you define the characteristics of your servers once. Then, when you
|
13
|
+
need to use Puppet, Capistrano drives. It deploys your Puppet modules and
|
14
|
+
manifests, plus a special host-specific file, and applies it to the server.
|
15
|
+
(This is sometimes called "masterless Puppet". It has a lot of benefits that
|
16
|
+
derive from decentralization and pushing changes on-demand.)
|
17
|
+
|
18
|
+
Here's what your config file might look like:
|
19
|
+
|
20
|
+
# An application called 'readme' that uses Cap's default deployment recipe.
|
21
|
+
application('readme', :recipes => 'deploy') {
|
22
|
+
# Set a capistrano variable.
|
23
|
+
cap_set('repository', 'git@github.com:joseph/readme.git')
|
24
|
+
|
25
|
+
# Declare that all servers will have the 'baseline' puppet class.
|
26
|
+
role(:puppet => 'baseline')
|
27
|
+
|
28
|
+
group('staging') {
|
29
|
+
# Puppet gets a $::exception_subject_prefix variable on these servers.
|
30
|
+
param('exception_subject_prefix' => '[STAGING] ')
|
31
|
+
# For simple staging, just one server that does everything.
|
32
|
+
server('readme.stage') {
|
33
|
+
role(:cap => [:web, :app, :db], :puppet => ['proxy', 'app', 'db'])
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
group('production') {
|
38
|
+
# Puppet gets these top-scope variables on servers in this group.
|
39
|
+
param(
|
40
|
+
'exception_subject_prefix' => '[PRODUCTION] ',
|
41
|
+
'env' => {
|
42
|
+
"FORCE_SSL" => true,
|
43
|
+
"S3_KEY" => "AKIAKJRK23943202JK",
|
44
|
+
"S3_SECRET" => "KDJkaddsalkjfkawjri32jkjaklvjgakljkj"
|
45
|
+
}
|
46
|
+
)
|
47
|
+
|
48
|
+
group('proxy') {
|
49
|
+
# Servers will have the :web role and the 'proxy' puppet class.
|
50
|
+
role(:cap => :web, :puppet => 'proxy')
|
51
|
+
server('proxy-1', :address => '10.10.10.5')
|
52
|
+
}
|
53
|
+
|
54
|
+
group('app') {
|
55
|
+
# Servers will have the :app role and the 'app' puppet class.
|
56
|
+
role(:app)
|
57
|
+
server('app-1', :address => '10.10.10.10')
|
58
|
+
server('app-2', :address => '10.10.10.11')
|
59
|
+
}
|
60
|
+
|
61
|
+
group('db') {
|
62
|
+
role(:db)
|
63
|
+
server('db-1', :address => '10.10.10.50')
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
|
69
|
+
Save this as `hub/example.rb`.
|
70
|
+
|
71
|
+
Run:
|
72
|
+
|
73
|
+
$ hubcap ALL servers:tree
|
74
|
+
|
75
|
+
That's a lot of info. You can filter your server list to target specific
|
76
|
+
groups of servers:
|
77
|
+
|
78
|
+
$ `hubcap example.vagrant servers:tree`
|
79
|
+
|
80
|
+
You can run `list` in place of `tree` to see just the servers that match
|
81
|
+
your filter:
|
82
|
+
|
83
|
+
$ `hubcap example.production.db servers:tree`
|
84
|
+
|
85
|
+
|
86
|
+
## Working with Puppet
|
87
|
+
|
88
|
+
You should have your Puppet modules in a git repository. The location of this
|
89
|
+
repository should be specified in your Capfile with
|
90
|
+
`set(:puppet_repository, '...')`. Your site manifest should be within this repo
|
91
|
+
at `puppet/host.pp` (but this is also configurable).
|
92
|
+
|
93
|
+
When you're ready to provision some servers:
|
94
|
+
|
95
|
+
$ `hubcap example.vagrant puppet:noop`
|
96
|
+
$ `hubcap example.vagrant puppet:apply`
|
97
|
+
|
98
|
+
Once that's done, you can deploy your app in the usual way:
|
99
|
+
|
100
|
+
$ `hubcap example.vagrant deploy:setup deploy:cold`
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
## The Hubcap DSL
|
105
|
+
|
106
|
+
The Hubcap DSL is very simple. This is the basic set of statements:
|
107
|
+
|
108
|
+
* `group` - A named set of servers, roles, variables, attributes. Groups
|
109
|
+
can be nested.
|
110
|
+
|
111
|
+
* `application` - A special kind of group. You can pass `:recipes => ...`
|
112
|
+
to this declaration. Each recipe path will be loaded into Capistrano only
|
113
|
+
for this application. Applications can't be nested.
|
114
|
+
|
115
|
+
* `server` - An actual host that you are managing with Capistrano and
|
116
|
+
Puppet. The first argument is the name, which can be an IP address or domain
|
117
|
+
name if you like. Otherwise, pass `:address => '...'`.
|
118
|
+
|
119
|
+
* `cap_set` - Set a Capistrano variable.
|
120
|
+
|
121
|
+
* `cap_attribute` - Set a Cap attribute on all the servers within this
|
122
|
+
group, such as `:primary => true` or `:no_release => true`.
|
123
|
+
|
124
|
+
* `role` - Add a role to the list of Capistrano roles for servers within
|
125
|
+
this group. By default, these roles are supplied as classes to apply to the
|
126
|
+
host in Puppet. You can specify that a role is Capistrano-only with
|
127
|
+
`:cap => '...'`, or Puppet-only with :puppet => `'...'`. This is additive:
|
128
|
+
if you have multiple role declarations in your tree, all of them apply.
|
129
|
+
|
130
|
+
* `param` - Add to a hash of 'parameters' that will be supplied to Puppet
|
131
|
+
as top-scope variables for servers in this group. Like `role`, this is
|
132
|
+
additive.
|
133
|
+
|
134
|
+
Hubcap uses Puppet's External Node Classifier (ENC) feature to provide the
|
135
|
+
list of classes and parameters for a specific host. More info here:
|
136
|
+
http://docs.puppetlabs.com/guides/external_nodes.html
|
137
|
+
|
138
|
+
|
139
|
+
## Hubcap as a library
|
140
|
+
|
141
|
+
If you'd rather run `cap` than `hubcap`, you can load your hub configuration
|
142
|
+
directly in your `Capfile`. Add this to the end of the file:
|
143
|
+
|
144
|
+
require('hubcap')
|
145
|
+
Hubcap.load('', 'hub').configure_capistrano(self)
|
146
|
+
|
147
|
+
The two arguments to `Hubcap.load` are the filter (where `''` means no filter),
|
148
|
+
and the path to the hub configuration. This will load `*.rb` in the `hub`
|
149
|
+
directory (but not subdirectories). You can specify multiple paths as additional
|
150
|
+
arguments -- whole directories or specific files.
|
151
|
+
|
152
|
+
If you want to simulate the behaviour of the `hubcap` script, you could do it
|
153
|
+
with something like this in your `Capfile`.
|
154
|
+
|
155
|
+
# Load servers and sets from node config. Any recipes loaded after this
|
156
|
+
# point will be available only in application mode.
|
157
|
+
if (target = ENV['TO']) && !ENV['TO'].empty?
|
158
|
+
target = '' if target == 'ALL'
|
159
|
+
require('hubcap')
|
160
|
+
Hubcap.load(target, 'hub').configure_capistrano(self)
|
161
|
+
else
|
162
|
+
warn("NB: No servers specified. Target a Hubcap group with TO.")
|
163
|
+
end
|
164
|
+
|
165
|
+
In this set-up, you'd run `cap` like this:
|
166
|
+
|
167
|
+
$ cap TO=example.vagrant servers:tree
|
168
|
+
|
data/Rakefile
ADDED
data/bin/hubcap
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# 'hubcap' - a convenience script for loading Capistrano with Hubcap config.
|
4
|
+
#
|
5
|
+
# Usages:
|
6
|
+
#
|
7
|
+
# # This will list the available default tasks:
|
8
|
+
# hubcap -T
|
9
|
+
#
|
10
|
+
# # This task (or tasks) will run on all defined servers in the hub:
|
11
|
+
# hubcap ALL task:name ...
|
12
|
+
#
|
13
|
+
# # This task will run on all servers inside the specified group:
|
14
|
+
# hubcap app_name.group_name task:name ...
|
15
|
+
#
|
16
|
+
# # This will run on just the specified server:
|
17
|
+
# hubcap app_name.group_name.server_name task:name ...
|
18
|
+
#
|
19
|
+
#
|
20
|
+
# Note that the hub configuration files are loaded from a subdirectory of the
|
21
|
+
# current directory named 'hub'. You can change this to point at another
|
22
|
+
# directory, single file or multiple files by setting the HUB_CONFIG env var.
|
23
|
+
#
|
24
|
+
# HUB_CONFIG=test/data hubcap ALL servers:list
|
25
|
+
#
|
26
|
+
# HUB_CONFIG=test/data/example.rb,test/data/simple.rb hubcap ALL servers:tree
|
27
|
+
#
|
28
|
+
#
|
29
|
+
|
30
|
+
|
31
|
+
require('capistrano/cli')
|
32
|
+
require('hubcap')
|
33
|
+
|
34
|
+
|
35
|
+
class Hubcap::CLI < Capistrano::CLI
|
36
|
+
|
37
|
+
attr_accessor(:cap)
|
38
|
+
|
39
|
+
|
40
|
+
def self.roll!
|
41
|
+
target = pre_parse_for_target(ARGV)
|
42
|
+
|
43
|
+
if !target
|
44
|
+
puts("Usage: hubcap name.of.target.group task:name")
|
45
|
+
puts("To target all servers: hubcap ALL task:name")
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
|
49
|
+
if target == :skip
|
50
|
+
parse(ARGV).execute!
|
51
|
+
else
|
52
|
+
filter = (target == 'ALL') ? '' : target
|
53
|
+
paths = ['hub']
|
54
|
+
paths = ENV['HUB_CONFIG'].split(',') if ENV['HUB_CONFIG']
|
55
|
+
hub = Hubcap.load(filter, *paths)
|
56
|
+
unless hub.children.any?
|
57
|
+
puts("Hubcap error: no servers for '#{target}' in [#{paths.join(',')}]")
|
58
|
+
exit
|
59
|
+
end
|
60
|
+
parse(ARGV) { |cap|
|
61
|
+
cap.load('standard')
|
62
|
+
hub.configure_capistrano(cap)
|
63
|
+
}.execute!
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
def self.pre_parse_for_target(args)
|
69
|
+
if args.length == 1 && args.first.match(/^-/)
|
70
|
+
return :skip
|
71
|
+
elsif args.length < 1
|
72
|
+
puts("Error: no servers specified")
|
73
|
+
return nil
|
74
|
+
elsif args.length < 2
|
75
|
+
puts("Error: no tasks specified")
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
return args.shift
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def self.parse(args)
|
83
|
+
cli = new(args)
|
84
|
+
cli.parse_options!
|
85
|
+
cli.cap = Capistrano::Configuration.new(cli.options)
|
86
|
+
yield(cli.cap) if block_given?
|
87
|
+
cli
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def instantiate_configuration(options = {})
|
92
|
+
self.cap
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
Hubcap::CLI.roll!
|
data/hubcap.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/hubcap/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ['Joseph Pearson']
|
6
|
+
gem.email = ['joseph@booki.sh']
|
7
|
+
gem.description = 'Unite Capistrano and Puppet config in one Ruby file.'
|
8
|
+
gem.summary = 'Hubcap Capistrano/Puppet extension'
|
9
|
+
gem.homepage = ''
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = 'hubcap'
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
gem.version = Hubcap::VERSION
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Hubcap::Application < Hubcap::Group
|
2
|
+
|
3
|
+
attr_reader(:recipe_paths)
|
4
|
+
|
5
|
+
def initialize(parent, name, options = {}, &blk)
|
6
|
+
@recipe_paths = [options[:recipes]].flatten.compact
|
7
|
+
super(parent, name, &blk)
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def application(*args)
|
12
|
+
raise(Hubcap::NestedApplicationDisallowed)
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def extend_tree(outs)
|
17
|
+
outs << "Load: #{@recipe_paths.inspect}" if @recipe_paths.any?
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
class Hubcap::NestedApplicationDisallowed < StandardError; end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
data/lib/hubcap/group.rb
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
class Hubcap::Group
|
2
|
+
|
3
|
+
attr_reader(
|
4
|
+
:name,
|
5
|
+
:cap_attributes,
|
6
|
+
:cap_roles,
|
7
|
+
:puppet_roles,
|
8
|
+
:params,
|
9
|
+
:parent,
|
10
|
+
:children
|
11
|
+
)
|
12
|
+
|
13
|
+
# Supply the parent group, the name of this new group and a block of code to
|
14
|
+
# evaluate in the context of this new group.
|
15
|
+
#
|
16
|
+
# Every group must have a parent group, unless it is the top-most group: the
|
17
|
+
# hub. The hub must be a Hubcap::Hub.
|
18
|
+
#
|
19
|
+
def initialize(parent, name, &blk)
|
20
|
+
@name = name.to_s
|
21
|
+
if @parent = parent
|
22
|
+
@cap_attributes = parent.cap_attributes.clone
|
23
|
+
@cap_roles = parent.cap_roles.clone
|
24
|
+
@puppet_roles = parent.puppet_roles.clone
|
25
|
+
@params = parent.params.clone
|
26
|
+
elsif !kind_of?(Hubcap::Hub)
|
27
|
+
raise(Hubcap::GroupWithoutParent, self.inspect)
|
28
|
+
end
|
29
|
+
@children = []
|
30
|
+
instance_eval(&blk) if blk && processable?
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Load a Ruby file and evaluate it in the context of this group.
|
35
|
+
# Like Ruby's require(), the '.rb' is optional in the path.
|
36
|
+
#
|
37
|
+
def absorb(path)
|
38
|
+
p = path
|
39
|
+
p += '.rb' unless File.exists?(p)
|
40
|
+
raise("File not found: #{path}") unless File.exists?(p)
|
41
|
+
code = IO.read(p)
|
42
|
+
eval(code)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Finds the top-level Hubcap::Hub to which this group belongs.
|
47
|
+
#
|
48
|
+
def hub
|
49
|
+
@parent ? @parent.hub : self
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# An array of names, from the oldest ancestor to the parent to self.
|
54
|
+
#
|
55
|
+
def history
|
56
|
+
@parent ? @parent.history + [@name] : []
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Indicates whether we should process this group. We process all groups that
|
61
|
+
# match the filter or are below the furthest point in the filter.
|
62
|
+
#
|
63
|
+
# "Match the filter" means that this group's history and the filter are
|
64
|
+
# identical to the end of the shortest of the two arrays.
|
65
|
+
#
|
66
|
+
def processable?
|
67
|
+
s = [history.length, hub.filter.length].min
|
68
|
+
history.slice(0,s) == hub.filter.slice(0,s)
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Indicates whether we should store this group in the hub's array of
|
73
|
+
# groups/applications/servers. We only store groups at the end of the filter
|
74
|
+
# and below.
|
75
|
+
#
|
76
|
+
# That is, this group's history should be the same length or longer than the
|
77
|
+
# filter, but identical at each point in the filter.
|
78
|
+
#
|
79
|
+
def collectable?
|
80
|
+
(history.length >= hub.filter.length) && processable?
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
# Sets a variable in the Capistrano instance.
|
85
|
+
#
|
86
|
+
# Note: when Hubcap is in application mode (not executing a default task),
|
87
|
+
# an exception will be raised if a variable is set twice to two different
|
88
|
+
# values.
|
89
|
+
#
|
90
|
+
# Either:
|
91
|
+
# cap_set(:foo, 'bar')
|
92
|
+
# or:
|
93
|
+
# cap_set(:foo => 'bar')
|
94
|
+
# and this works too:
|
95
|
+
# cap_set(:foo => 'bar', :garply => 'grault')
|
96
|
+
# in fact, even this works:
|
97
|
+
# cap_set(:foo) { bar }
|
98
|
+
#
|
99
|
+
def cap_set(*args, &blk)
|
100
|
+
if args.length == 2
|
101
|
+
hub.cap_set(args.first => args.last)
|
102
|
+
elsif args.length == 1
|
103
|
+
if block_given?
|
104
|
+
hub.cap_set(args.first => blk)
|
105
|
+
elsif args.first.kind_of?(Hash)
|
106
|
+
hub.cap_set(args.first)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
raise ArgumentError('Must be (key, value) or (hash) or (key) { block }.')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Sets an attribute in the Capistrano server() definition for all Hubcap
|
115
|
+
# servers to which it applies.
|
116
|
+
#
|
117
|
+
# For eg, :primary => true or :no_release => true.
|
118
|
+
#
|
119
|
+
# Either:
|
120
|
+
# cap_attribute(:foo, 'bar')
|
121
|
+
# or:
|
122
|
+
# cap_attribute(:foo => 'bar')
|
123
|
+
# and this works too:
|
124
|
+
# cap_attribute(:foo => 'bar', :garply => 'grault')
|
125
|
+
#
|
126
|
+
def cap_attribute(*args)
|
127
|
+
if args.length == 2
|
128
|
+
cap_attribute(args.first => args.last)
|
129
|
+
elsif args.length == 1 && args.first.kind_of?(Hash)
|
130
|
+
@cap_attributes.update(args.first)
|
131
|
+
else
|
132
|
+
raise ArgumentError('Must be (key, value) or (hash).')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# Sets the Capistrano role and/or Puppet class for all Hubcap servers to
|
138
|
+
# which it applies.
|
139
|
+
#
|
140
|
+
# When declared multiple times (even in parents), it's additive.
|
141
|
+
#
|
142
|
+
# Either:
|
143
|
+
# role(:app)
|
144
|
+
# or:
|
145
|
+
# role(:app, :db)
|
146
|
+
# or:
|
147
|
+
# role(:cap => :app, :puppet => 'relishapp')
|
148
|
+
# or:
|
149
|
+
# role(:cap => [:app, :db], :puppet => 'relishapp')
|
150
|
+
#
|
151
|
+
def role(*args)
|
152
|
+
if args.length == 1 && args.first.kind_of?(Hash)
|
153
|
+
h = args.first
|
154
|
+
@cap_roles += [h[:cap]].flatten if h.has_key?(:cap)
|
155
|
+
@puppet_roles += [h[:puppet]].flatten if h.has_key?(:puppet)
|
156
|
+
else
|
157
|
+
@cap_roles += args
|
158
|
+
@puppet_roles += args
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# Adds values to a hash that is supplied to Puppet when it is provisioning
|
164
|
+
# the server.
|
165
|
+
#
|
166
|
+
# If you do this...
|
167
|
+
# params(:foo => 'bar')
|
168
|
+
# ...then Puppet will have a top-level variable called $foo, containing 'bar'.
|
169
|
+
#
|
170
|
+
def param(hash)
|
171
|
+
@params.update(hash)
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
# Instantiate an application as a child of this group.
|
176
|
+
#
|
177
|
+
def application(name, options = {}, &blk)
|
178
|
+
add_child(:applications, Hubcap::Application.new(self, name, options, &blk))
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# Instantiate a server as a child of this group.
|
183
|
+
#
|
184
|
+
def server(name, options = {}, &blk)
|
185
|
+
add_child(:servers, Hubcap::Server.new(self, name, options, &blk))
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# Instantiate a group as a child of this group.
|
190
|
+
#
|
191
|
+
def group(name, &blk)
|
192
|
+
add_child(:groups, Hubcap::Group.new(self, name, &blk))
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
# Returns a formatted string of all the key details for this group, and
|
197
|
+
# recurses into each child.
|
198
|
+
#
|
199
|
+
def tree(indent = " ")
|
200
|
+
outs = [self.class.name.split('::').last.upcase, "Name: #{@name}"]
|
201
|
+
outs << "Atts: #{@cap_attributes.inspect}" if @cap_attributes.any?
|
202
|
+
outs << "Pram: #{@params.inspect}" if @params.any?
|
203
|
+
extend_tree(outs) if respond_to?(:extend_tree)
|
204
|
+
outs << ""
|
205
|
+
if @children.any?
|
206
|
+
@children.each { |child| outs << child.tree(indent+" ") }
|
207
|
+
end
|
208
|
+
outs.join("\n#{indent}")
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def add_child(category, child)
|
215
|
+
@children << child if child.processable?
|
216
|
+
hub.send(category) << child if child.collectable?
|
217
|
+
child
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
class Hubcap::GroupWithoutParent < StandardError; end
|
222
|
+
|
223
|
+
end
|
data/lib/hubcap/hub.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
class Hubcap::Hub < Hubcap::Group
|
2
|
+
|
3
|
+
attr_reader(:filter, :applications, :servers, :groups, :cap_sets)
|
4
|
+
|
5
|
+
|
6
|
+
def initialize(filter_string)
|
7
|
+
@filter = filter_string.split('.')
|
8
|
+
@cap_sets = {}
|
9
|
+
@cap_set_clashes = []
|
10
|
+
@cap_attributes = {}
|
11
|
+
@cap_roles = []
|
12
|
+
@puppet_roles = []
|
13
|
+
@params = {}
|
14
|
+
@applications = []
|
15
|
+
@servers = []
|
16
|
+
@groups = []
|
17
|
+
super(nil, '∞')
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def cap_set(hash)
|
22
|
+
hash.each_pair { |k, v|
|
23
|
+
if @cap_sets[k] && @cap_sets[k] != v
|
24
|
+
@cap_set_clashes << { k => v }
|
25
|
+
else
|
26
|
+
@cap_sets[k] = v
|
27
|
+
end
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def extend_tree(outs)
|
33
|
+
outs << "Sets: #{@cap_sets.inspect}"
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# Does a few things:
|
38
|
+
#
|
39
|
+
# * Sets the :hubcap variable in the Capistrano instance.
|
40
|
+
# * Loads the default Hubcap recipes into the Capistrano instance.
|
41
|
+
# * Defines each server as a Capistrano server().
|
42
|
+
#
|
43
|
+
# If we are in "agnostic mode" - executing a default task - that's all we
|
44
|
+
# do. If we are in "application mode" - executing at least one non-standard
|
45
|
+
# task - then we do a few more things:
|
46
|
+
#
|
47
|
+
# * Load all the recipes for the application into the Capistrano instance.
|
48
|
+
# * Set all the @cap_set variables in the Capistrano instance.
|
49
|
+
#
|
50
|
+
def configure_capistrano(cap)
|
51
|
+
raise(Hubcap::CapistranoAlreadyConfigured) if cap.exists?(:hubcap)
|
52
|
+
cap.set(:hubcap, self)
|
53
|
+
|
54
|
+
cap.instance_eval {
|
55
|
+
require('hubcap/recipes/servers')
|
56
|
+
require('hubcap/recipes/puppet')
|
57
|
+
}
|
58
|
+
|
59
|
+
# Declare the servers.
|
60
|
+
servers.each { |s|
|
61
|
+
cap.server(s.address, *(s.cap_roles + [s.cap_attributes]))
|
62
|
+
}
|
63
|
+
|
64
|
+
configure_application_mode(cap) unless capistrano_is_agnostic?(cap)
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# In agnostic mode, Capistrano recipes for specific applications are not
|
69
|
+
# loaded, and cap_set collisions are ignored.
|
70
|
+
#
|
71
|
+
def capistrano_is_agnostic?(cap)
|
72
|
+
return cap.fetch(:hubcap_agnostic) if cap.exists?(:hubcap_agnostic)
|
73
|
+
ag = true
|
74
|
+
options = cap.logger.instance_variable_get(:@options)
|
75
|
+
if options && options[:actions] && options[:actions].any?
|
76
|
+
tasks = options[:actions].clone
|
77
|
+
while tasks.any?
|
78
|
+
ag = false unless cap.find_task(tasks.shift)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
cap.set(:hubcap_agnostic, ag)
|
82
|
+
ag
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def configure_application_mode(cap)
|
89
|
+
apps = servers.collect(&:application_parent).compact.uniq
|
90
|
+
|
91
|
+
# A - there should be only one application for all the servers
|
92
|
+
raise(
|
93
|
+
Hubcap::ApplicationModeError::TooManyApplications,
|
94
|
+
apps.collect(&:name).join(', ')
|
95
|
+
) if apps.size > 1
|
96
|
+
|
97
|
+
# B - there should be no clash of cap sets
|
98
|
+
raise(
|
99
|
+
Hubcap::ApplicationModeError::DuplicateSets,
|
100
|
+
@cap_set_clashes.inspect
|
101
|
+
) if @cap_set_clashes.any?
|
102
|
+
|
103
|
+
# C - app-specific, but no applications
|
104
|
+
raise(Hubcap::ApplicationModeError::NoApplications) if !apps.any?
|
105
|
+
|
106
|
+
# Otherwise, load all recipes...
|
107
|
+
cap.set(:application, apps.first.name)
|
108
|
+
apps.first.recipe_paths.each { |rp| cap.load(rp) }
|
109
|
+
|
110
|
+
# ..and declare all cap sets.
|
111
|
+
@cap_sets.each_pair { |key, val|
|
112
|
+
val.kind_of?(Proc) ? cap.set(key, &val) : cap.set(key, val)
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
class Hubcap::CapistranoAlreadyConfigured < StandardError; end
|
119
|
+
class Hubcap::ApplicationModeError < StandardError;
|
120
|
+
class TooManyApplications < self; end
|
121
|
+
class NoApplications < self; end
|
122
|
+
class DuplicateSets < self; end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|