hubcap 0.0.1
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/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
|