qb 0.3.4 → 0.3.5
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.
- checksums.yaml +4 -4
- data/ansible.cfg +0 -1
- data/exe/qb +22 -306
- data/lib/qb/ansible/cmds/playbook.rb +59 -8
- data/lib/qb/cli.rb +26 -3
- data/lib/qb/cli/help.rb +56 -0
- data/lib/qb/cli/play.rb +22 -10
- data/lib/qb/cli/run.rb +291 -0
- data/lib/qb/cli/setup.rb +57 -0
- data/lib/qb/role.rb +859 -771
- data/lib/qb/version.rb +1 -1
- data/qb.gemspec +1 -1
- data/roles/qb/dev/ref/repo/git/defaults/main.yml +5 -4
- data/roles/qb/dev/ref/repo/git/tasks/main.yml +9 -0
- data/roles/qb/yarn/setup/README.md +47 -0
- data/roles/{qb.yarn_setup → qb/yarn/setup}/defaults/main.yml +1 -1
- data/roles/{qb.yarn_setup → qb/yarn/setup}/meta/main.yml +1 -1
- data/roles/{qb.yarn_setup → qb/yarn/setup}/meta/qb.yml +2 -2
- data/roles/{qb.yarn_setup → qb/yarn/setup}/tasks/distribution/MacOSX/brew_create.yml +1 -1
- data/roles/qb/yarn/setup/tasks/distribution/MacOSX/main.yml +57 -0
- data/roles/{qb.yarn_setup → qb/yarn/setup}/tasks/main.yml +1 -1
- data/roles/qb/yarn/setup/templates/yarn@M.m.p.rb.j2 +29 -0
- metadata +14 -11
- data/roles/qb.yarn_setup/tasks/distribution/MacOSX/main.yml +0 -42
- data/roles/qb.yarn_setup/templates/yarn@M.m.p.rb.j2 +0 -22
data/lib/qb/cli/setup.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Requirements
|
2
|
+
# =====================================================================
|
3
|
+
|
4
|
+
# package
|
5
|
+
require 'qb/ansible/cmds/playbook'
|
6
|
+
|
7
|
+
|
8
|
+
# Declarations
|
9
|
+
# =======================================================================
|
10
|
+
|
11
|
+
module QB; end
|
12
|
+
|
13
|
+
|
14
|
+
# Definitions
|
15
|
+
# =======================================================================
|
16
|
+
|
17
|
+
module QB::CLI
|
18
|
+
|
19
|
+
# Play `//dev/setup.yml`
|
20
|
+
#
|
21
|
+
# @param [Array<String>] args
|
22
|
+
# CLI arguments to use.
|
23
|
+
#
|
24
|
+
# @return [Fixnum]
|
25
|
+
# The `ansible-playbook` command exit code.
|
26
|
+
#
|
27
|
+
def self.setup args = []
|
28
|
+
project_root = NRSER.git_root '.'
|
29
|
+
playbook_path = project_root / 'dev' / 'setup.qb.yml'
|
30
|
+
|
31
|
+
unless playbook_path.file?
|
32
|
+
raise "Can't find QB setup playbook at `#{ playbook_path.to_s }`"
|
33
|
+
end
|
34
|
+
|
35
|
+
cmd = QB::Ansible::Cmds::Playbook.new \
|
36
|
+
chdir: project_root,
|
37
|
+
extra_vars: {
|
38
|
+
project_root: project_root,
|
39
|
+
qb_dir: project_root,
|
40
|
+
qb_cwd: Pathname.getwd,
|
41
|
+
qb_user_roles_dir: QB::USER_ROLES_DIR,
|
42
|
+
},
|
43
|
+
playbook_path: playbook_path
|
44
|
+
|
45
|
+
puts cmd.prepare
|
46
|
+
|
47
|
+
status = cmd.stream
|
48
|
+
|
49
|
+
if status != 0
|
50
|
+
$stderr.puts "ERROR QB setup failed."
|
51
|
+
end
|
52
|
+
|
53
|
+
exit status
|
54
|
+
|
55
|
+
end # .setup
|
56
|
+
|
57
|
+
end # module QB::CLI
|
data/lib/qb/role.rb
CHANGED
@@ -1,857 +1,945 @@
|
|
1
|
+
|
2
|
+
# Requirements
|
3
|
+
# =======================================================================
|
4
|
+
|
5
|
+
# stdlib
|
1
6
|
require 'yaml'
|
2
7
|
require 'cmds'
|
3
|
-
require 'parseconfig'
|
4
8
|
|
5
|
-
|
9
|
+
# deps
|
6
10
|
|
7
|
-
|
11
|
+
# package
|
12
|
+
require_relative './role/errors'
|
13
|
+
|
14
|
+
|
15
|
+
# Refinements
|
16
|
+
# =======================================================================
|
8
17
|
|
18
|
+
require 'nrser/refinements'
|
9
19
|
using NRSER
|
10
20
|
|
11
|
-
|
12
|
-
|
21
|
+
|
22
|
+
# Declarations
|
23
|
+
# =======================================================================
|
24
|
+
|
25
|
+
module QB; end
|
26
|
+
|
27
|
+
|
28
|
+
# Contains info on a QB role.
|
29
|
+
#
|
30
|
+
class QB::Role
|
31
|
+
|
32
|
+
# Constants
|
33
|
+
# =====================================================================
|
34
|
+
|
35
|
+
# Array of string paths to directories to search for roles or paths to
|
36
|
+
# `ansible.cfg` files to look for an extract role paths from.
|
13
37
|
#
|
38
|
+
# For the moment at least you can just mutate this value like you would
|
39
|
+
# `$LOAD_PATH`:
|
14
40
|
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
41
|
+
# QB::Role::PATH.unshift '~/where/some/roles/be'
|
42
|
+
# QB::Role::PATH.unshift '~/my/ansible.cfg'
|
43
|
+
#
|
44
|
+
# The paths are searched from first to last.
|
45
|
+
#
|
46
|
+
# **WARNING**
|
47
|
+
#
|
48
|
+
# Search is **deep** - don't point this at large directory trees and
|
49
|
+
# expect any sort of reasonable performance (any directory that
|
50
|
+
# contains `node_modules` is usually a terrible idea for instance).
|
51
|
+
#
|
52
|
+
BUILTIN_PATH = [
|
19
53
|
|
20
|
-
#
|
21
|
-
#
|
54
|
+
# Development Paths
|
55
|
+
# =================
|
22
56
|
#
|
23
|
-
#
|
24
|
-
# `$LOAD_PATH`:
|
57
|
+
# These come first because:
|
25
58
|
#
|
26
|
-
#
|
27
|
-
# QB::Role::PATH.unshift '~/my/ansible.cfg'
|
59
|
+
# 1. They are working dir-local.
|
28
60
|
#
|
29
|
-
#
|
61
|
+
# 2. They should only be present in local development, and should be
|
62
|
+
# capable of overriding roles in other local directories to allow
|
63
|
+
# custom development behavior (the same way `./dev/bin` is put in
|
64
|
+
# front or `./bin`).
|
30
65
|
#
|
31
|
-
# **WARNING**
|
32
|
-
#
|
33
|
-
# Search is **deep** - don't point this at large directory trees and
|
34
|
-
# expect any sort of reasonable performance (any directory that
|
35
|
-
# contains `node_modules` is usually a terrible idea for instance).
|
36
|
-
#
|
37
|
-
BUILTIN_PATH = [
|
38
|
-
|
39
|
-
# Development Paths
|
40
|
-
# =================
|
41
|
-
#
|
42
|
-
# These come first because:
|
43
|
-
#
|
44
|
-
# 1. They are working dir-local.
|
45
|
-
#
|
46
|
-
# 2. They should only be present in local development, and should be
|
47
|
-
# capable of overriding roles in other local directories to allow
|
48
|
-
# custom development behavior (the same way `./dev/bin` is put in
|
49
|
-
# front or `./bin`).
|
50
|
-
#
|
51
|
-
|
52
|
-
# Role paths declared in ./dev/ansible.cfg, if it exists.
|
53
|
-
File.join('.', 'dev', 'ansible.cfg'),
|
54
|
-
|
55
|
-
# Roles in ./dev/roles
|
56
|
-
File.join('.', 'dev', 'roles'),
|
57
|
-
|
58
|
-
|
59
|
-
# Working Directory Paths
|
60
|
-
# =======================
|
61
|
-
#
|
62
|
-
# Next up, `ansible.cfg` and `roles` directory in the working dir.
|
63
|
-
# Makes sense, right?
|
64
|
-
#
|
65
|
-
|
66
|
-
# ./ansible.cfg
|
67
|
-
File.join('.', 'ansible.cfg'),
|
68
|
-
|
69
|
-
# ./roles
|
70
|
-
File.join('.', 'roles'),
|
71
|
-
|
72
|
-
|
73
|
-
# Working Directory-Local Ansible Directory
|
74
|
-
# =========================================
|
75
|
-
#
|
76
|
-
# `ansible.cfg` and `roles` in a `./ansible` directory, making a common
|
77
|
-
# place to put Ansible stuff in an project accessible when running from
|
78
|
-
# the project root.
|
79
|
-
#
|
80
|
-
|
81
|
-
# ./ansible/ansible.cfg
|
82
|
-
File.join('.', 'ansible', 'ansible.cfg'),
|
83
|
-
|
84
|
-
# ./ansible/roles
|
85
|
-
File.join('.', 'ansible', 'roles'),
|
86
|
-
|
87
|
-
# TODO Git repo root relative?
|
88
|
-
# Some sort of flag file for a find-up?
|
89
|
-
# System Ansible locations?
|
90
|
-
|
91
|
-
|
92
|
-
# QB Gem Role Directories
|
93
|
-
# =======================
|
94
|
-
#
|
95
|
-
# Last, but far from least, paths provided by the QB Gem to the user's
|
96
|
-
# QB role install location and the roles that come built-in to the gem.
|
97
|
-
|
98
|
-
QB::USER_ROLES_DIR,
|
99
|
-
|
100
|
-
QB::GEM_ROLES_DIR,
|
101
|
-
].freeze
|
102
66
|
|
67
|
+
# Role paths declared in ./dev/ansible.cfg, if it exists.
|
68
|
+
File.join('.', 'dev', 'ansible.cfg'),
|
103
69
|
|
104
|
-
#
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# For the moment at least you can just mutate this value like you would
|
111
|
-
# `$LOAD_PATH`:
|
112
|
-
#
|
113
|
-
# QB::Role::PATH.unshift '~/where/some/roles/be'
|
114
|
-
# QB::Role::PATH.unshift '~/my/ansible.cfg'
|
115
|
-
#
|
116
|
-
# The paths are searched from first to last.
|
117
|
-
#
|
118
|
-
# **WARNING**
|
70
|
+
# Roles in ./dev/roles
|
71
|
+
File.join('.', 'dev', 'roles'),
|
72
|
+
|
73
|
+
|
74
|
+
# Working Directory Paths
|
75
|
+
# =======================
|
119
76
|
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
# contains `node_modules` is usually a terrible idea for instance).
|
77
|
+
# Next up, `ansible.cfg` and `roles` directory in the working dir.
|
78
|
+
# Makes sense, right?
|
123
79
|
#
|
124
|
-
PATH = BUILTIN_PATH.dup
|
125
|
-
|
126
80
|
|
127
|
-
#
|
128
|
-
|
81
|
+
# ./ansible.cfg
|
82
|
+
File.join('.', 'ansible.cfg'),
|
129
83
|
|
84
|
+
# ./roles
|
85
|
+
File.join('.', 'roles'),
|
130
86
|
|
131
|
-
# Reset {QB::Role::PATH} to the original built-in values in
|
132
|
-
# {QB::Role::BUILTIN_PATH}.
|
133
|
-
#
|
134
|
-
# Created for testing but might be useful elsewhere as well.
|
135
|
-
#
|
136
|
-
# @return [Array<String>]
|
137
|
-
# The reset {QB::Role::PATH}.
|
138
|
-
#
|
139
|
-
def self.reset_path!
|
140
|
-
PATH.clear
|
141
|
-
BUILTIN_PATH.each { |path| PATH << path }
|
142
|
-
PATH
|
143
|
-
end # .reset_path!
|
144
|
-
|
145
|
-
|
146
|
-
# true if pathname is a QB role directory.
|
147
|
-
def self.role_dir? pathname
|
148
|
-
# must be a directory
|
149
|
-
pathname.directory? &&
|
150
|
-
# and must have meta/qb.yml or meta/qb file
|
151
|
-
['qb.yml', 'qb'].any? {|filename| pathname.join('meta', filename).file?}
|
152
|
-
end
|
153
87
|
|
154
|
-
#
|
88
|
+
# Working Directory-Local Ansible Directory
|
89
|
+
# =========================================
|
155
90
|
#
|
156
|
-
#
|
91
|
+
# `ansible.cfg` and `roles` in a `./ansible` directory, making a common
|
92
|
+
# place to put Ansible stuff in an project accessible when running from
|
93
|
+
# the project root.
|
157
94
|
#
|
158
|
-
# @return [Array<String>]
|
159
|
-
# Absolute role paths.
|
160
|
-
#
|
161
|
-
def self.cfg_roles_paths path
|
162
|
-
path = File.join(path, 'ansible.cfg') unless File.basename(path) == 'ansible.cfg'
|
163
|
-
|
164
|
-
if path.file?
|
165
|
-
config = ParseConfig.new path.to_s
|
166
|
-
config['defaults']['roles_path'].split(':').map {|path|
|
167
|
-
QB::Util.resolve dir, path
|
168
|
-
}
|
169
|
-
else
|
170
|
-
[]
|
171
|
-
end
|
172
|
-
end
|
173
95
|
|
174
|
-
#
|
175
|
-
|
176
|
-
cfg_roles_path(dir) + [
|
177
|
-
dir.join('roles'),
|
178
|
-
dir.join('roles', 'tmp'),
|
179
|
-
]
|
180
|
-
end
|
96
|
+
# ./ansible/ansible.cfg
|
97
|
+
File.join('.', 'ansible', 'ansible.cfg'),
|
181
98
|
|
182
|
-
#
|
183
|
-
|
184
|
-
#
|
185
|
-
# TODO resolution order:
|
186
|
-
#
|
187
|
-
# 1. paths specific to this run:
|
188
|
-
# a. TODO paths provided on the cli.
|
189
|
-
# 2. paths specific to the current directory:
|
190
|
-
# a. paths specified in ./ansible.cfg (if it exists)
|
191
|
-
# b. ./roles
|
192
|
-
# d. paths specified in ./ansible/ansible.cfg (if it exists)
|
193
|
-
# e. ./ansible/roles
|
194
|
-
# g. paths specified in ./dev/ansible.cfg (if it exists)
|
195
|
-
# h. ./dev/roles
|
196
|
-
# i. ./dev/roles/tmp
|
197
|
-
# - used for roles that are downloaded but shouldn't be included
|
198
|
-
# in source control.
|
199
|
-
# 3.
|
200
|
-
#
|
201
|
-
# @return [Array<Pathname>]
|
202
|
-
# places to look for role dirs.
|
203
|
-
#
|
204
|
-
def self.search_path
|
205
|
-
QB::Role::PATH.
|
206
|
-
map { |path|
|
207
|
-
if QB::Ansible::ConfigFile.end_with_config_file?(path)
|
208
|
-
if File.file?(path)
|
209
|
-
QB::Ansible::ConfigFile.new(path).defaults.roles_path
|
210
|
-
end
|
211
|
-
else
|
212
|
-
QB::Util.resolve path
|
213
|
-
end
|
214
|
-
}.
|
215
|
-
flatten.
|
216
|
-
reject(&:nil?)
|
217
|
-
end
|
99
|
+
# ./ansible/roles
|
100
|
+
File.join('.', 'ansible', 'roles'),
|
218
101
|
|
219
|
-
#
|
220
|
-
|
221
|
-
|
222
|
-
select {|search_dir|
|
223
|
-
# make sure it's there (and a directory)
|
224
|
-
search_dir.directory?
|
225
|
-
}.
|
226
|
-
map {|search_dir|
|
227
|
-
Pathname.glob(search_dir.join '**', 'meta', 'qb.yml').
|
228
|
-
map {|meta_path|
|
229
|
-
[meta_path.dirname.dirname, search_dir: search_dir]
|
230
|
-
}
|
231
|
-
}.
|
232
|
-
flatten(1).
|
233
|
-
map {|args|
|
234
|
-
QB::Role.new *args
|
235
|
-
}.
|
236
|
-
uniq
|
237
|
-
end
|
102
|
+
# TODO Git repo root relative?
|
103
|
+
# Some sort of flag file for a find-up?
|
104
|
+
# System Ansible locations?
|
238
105
|
|
239
|
-
|
240
|
-
#
|
241
|
-
#
|
106
|
+
|
107
|
+
# QB Gem Role Directories
|
108
|
+
# =======================
|
242
109
|
#
|
243
|
-
|
244
|
-
|
245
|
-
available = self.available
|
246
|
-
|
247
|
-
# first off, see if input matches any relative paths exactly
|
248
|
-
available.each {|role|
|
249
|
-
return [role] if role.display_path.to_s == input
|
250
|
-
}
|
251
|
-
|
252
|
-
# create an array of "separator" variations to try *exact* matching
|
253
|
-
# against. in order of preference:
|
254
|
-
#
|
255
|
-
# 1. exact input
|
256
|
-
# - this means if you ended up with roles that actually *are*
|
257
|
-
# differnetiated by '_/-' differences (which, IMHO, is a
|
258
|
-
# horrible fucking idea), you can get exactly what you ask for
|
259
|
-
# as a first priority
|
260
|
-
# 2. input with '-' changed to '_'
|
261
|
-
# - prioritized because convetion is to underscore-separate
|
262
|
-
# role names.
|
263
|
-
# 3. input with '_' changed to '-'
|
264
|
-
# - really just for convience's sake so you don't really have to
|
265
|
-
# remember what separator is used.
|
266
|
-
#
|
267
|
-
separator_variations = [
|
268
|
-
input,
|
269
|
-
input.gsub('-', '_'),
|
270
|
-
input.gsub('_', '-'),
|
271
|
-
]
|
272
|
-
|
273
|
-
separator_variations.each { |variation|
|
274
|
-
available.each { |role|
|
275
|
-
# exact match to full name
|
276
|
-
return [role] if role.name == variation
|
277
|
-
}.each { |role|
|
278
|
-
# exact match without the namespace prefix ('qb.' or similar)
|
279
|
-
return [role] if role.namespaceless == variation
|
280
|
-
}
|
281
|
-
}
|
282
|
-
|
283
|
-
# see if we prefix match any full names
|
284
|
-
separator_variations.each { |variation|
|
285
|
-
name_prefix_matches = available.select { |role|
|
286
|
-
role.name.start_with? variation
|
287
|
-
}
|
288
|
-
return name_prefix_matches unless name_prefix_matches.empty?
|
289
|
-
}
|
290
|
-
|
291
|
-
# see if we prefix match any name
|
292
|
-
separator_variations.each { |variation|
|
293
|
-
namespaceless_prefix_matches = available.select { |role|
|
294
|
-
role.namespaceless.start_with? variation
|
295
|
-
}
|
296
|
-
unless namespaceless_prefix_matches.empty?
|
297
|
-
return namespaceless_prefix_matches
|
298
|
-
end
|
299
|
-
}
|
300
|
-
|
301
|
-
# see if we prefix match any display paths
|
302
|
-
separator_variations.each { |variation|
|
303
|
-
path_prefix_matches = available.select { |role|
|
304
|
-
role.display_path.start_with? variation
|
305
|
-
}
|
306
|
-
return path_prefix_matches unless path_prefix_matches.empty?
|
307
|
-
}
|
308
|
-
|
309
|
-
# see if we word match any display paths
|
310
|
-
name_word_matches = available.select { |role|
|
311
|
-
QB::Util.words_start_with? role.display_path.to_s, input
|
312
|
-
}
|
313
|
-
return name_word_matches unless name_word_matches.empty?
|
314
|
-
|
315
|
-
# nada
|
316
|
-
[]
|
317
|
-
end # .matches
|
110
|
+
# Last, but far from least, paths provided by the QB Gem to the user's
|
111
|
+
# QB role install location and the roles that come built-in to the gem.
|
318
112
|
|
319
|
-
|
320
|
-
def self.require input
|
321
|
-
|
322
|
-
as_pathname = Pathname.new(input)
|
323
|
-
|
324
|
-
# allow a path to a role dir
|
325
|
-
if role_dir? as_pathname
|
326
|
-
return Role.new as_pathname
|
327
|
-
end
|
328
|
-
|
329
|
-
matches = self.matches input
|
330
|
-
|
331
|
-
role = case matches.length
|
332
|
-
when 0
|
333
|
-
raise NoMatchesError.new input
|
334
|
-
when 1
|
335
|
-
matches[0]
|
336
|
-
else
|
337
|
-
raise MultipleMatchesError.new input, matches
|
338
|
-
end
|
339
|
-
|
340
|
-
QB.debug "role match" => role
|
341
|
-
|
342
|
-
role
|
343
|
-
end
|
113
|
+
QB::USER_ROLES_DIR,
|
344
114
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
115
|
+
QB::GEM_ROLES_DIR,
|
116
|
+
].freeze
|
117
|
+
|
118
|
+
|
119
|
+
# Array of string paths to directories to search for roles or paths to
|
120
|
+
# `ansible.cfg` files to look for an extract role paths from.
|
121
|
+
#
|
122
|
+
# Value is a duplicate of the frozen {QB::Role::BUILTIN_PATH}. You can
|
123
|
+
# reset to those values at any time via {QB::Role.reset_path!}.
|
124
|
+
#
|
125
|
+
# For the moment at least you can just mutate this value like you would
|
126
|
+
# `$LOAD_PATH`:
|
127
|
+
#
|
128
|
+
# QB::Role::PATH.unshift '~/where/some/roles/be'
|
129
|
+
# QB::Role::PATH.unshift '~/my/ansible.cfg'
|
130
|
+
#
|
131
|
+
# The paths are searched from first to last.
|
132
|
+
#
|
133
|
+
# **WARNING**
|
134
|
+
#
|
135
|
+
# Search is **deep** - don't point this at large directory trees and
|
136
|
+
# expect any sort of reasonable performance (any directory that
|
137
|
+
# contains `node_modules` is usually a terrible idea for instance).
|
138
|
+
#
|
139
|
+
PATH = BUILTIN_PATH.dup
|
140
|
+
|
141
|
+
|
142
|
+
# Class Methods
|
143
|
+
# =======================================================================
|
144
|
+
|
145
|
+
# Reset {QB::Role::PATH} to the original built-in values in
|
146
|
+
# {QB::Role::BUILTIN_PATH}.
|
147
|
+
#
|
148
|
+
# Created for testing but might be useful elsewhere as well.
|
149
|
+
#
|
150
|
+
# @return [Array<String>]
|
151
|
+
# The reset {QB::Role::PATH}.
|
152
|
+
#
|
153
|
+
def self.reset_path!
|
154
|
+
PATH.clear
|
155
|
+
BUILTIN_PATH.each { |path| PATH << path }
|
156
|
+
PATH
|
157
|
+
end # .reset_path!
|
158
|
+
|
159
|
+
|
160
|
+
# true if pathname is a QB role directory.
|
161
|
+
def self.role_dir? pathname
|
162
|
+
# must be a directory
|
163
|
+
pathname.directory? &&
|
164
|
+
# and must have meta/qb.yml or meta/qb file
|
165
|
+
['qb.yml', 'qb'].any? {|filename| pathname.join('meta', filename).file?}
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
# @param dir [Pathname] dir to include.
|
170
|
+
def self.roles_paths dir
|
171
|
+
cfg_roles_path(dir) + [
|
172
|
+
dir.join('roles'),
|
173
|
+
dir.join('roles', 'tmp'),
|
174
|
+
]
|
175
|
+
end
|
176
|
+
|
177
|
+
# places to look for qb role directories. these paths are also included
|
178
|
+
# when qb runs a playbook.
|
179
|
+
#
|
180
|
+
# TODO resolution order:
|
181
|
+
#
|
182
|
+
# 1. paths specific to this run:
|
183
|
+
# a. TODO paths provided on the cli.
|
184
|
+
# 2. paths specific to the current directory:
|
185
|
+
# a. paths specified in ./ansible.cfg (if it exists)
|
186
|
+
# b. ./roles
|
187
|
+
# d. paths specified in ./ansible/ansible.cfg (if it exists)
|
188
|
+
# e. ./ansible/roles
|
189
|
+
# g. paths specified in ./dev/ansible.cfg (if it exists)
|
190
|
+
# h. ./dev/roles
|
191
|
+
# i. ./dev/roles/tmp
|
192
|
+
# - used for roles that are downloaded but shouldn't be included
|
193
|
+
# in source control.
|
194
|
+
# 3.
|
195
|
+
#
|
196
|
+
# @return [Array<Pathname>]
|
197
|
+
# places to look for role dirs.
|
198
|
+
#
|
199
|
+
def self.search_path
|
200
|
+
QB::Role::PATH.
|
201
|
+
map { |path|
|
202
|
+
if QB::Ansible::ConfigFile.end_with_config_file?(path)
|
203
|
+
if File.file?(path)
|
204
|
+
QB::Ansible::ConfigFile.new(path).defaults.roles_path
|
205
|
+
end
|
368
206
|
else
|
369
|
-
|
370
|
-
"bad 'as' value: #{ option_meta.inspect }"
|
207
|
+
QB::Util.resolve path
|
371
208
|
end
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
209
|
+
}.
|
210
|
+
flatten.
|
211
|
+
reject(&:nil?)
|
212
|
+
end
|
213
|
+
|
214
|
+
# array of QB::Role found in search path.
|
215
|
+
def self.available
|
216
|
+
search_path.
|
217
|
+
select {|search_dir|
|
218
|
+
# make sure it's there (and a directory)
|
219
|
+
search_dir.directory?
|
220
|
+
}.
|
221
|
+
map {|search_dir|
|
222
|
+
Pathname.glob(search_dir.join '**', 'meta', 'qb.yml').
|
223
|
+
map {|meta_path|
|
224
|
+
[meta_path.dirname.dirname, search_dir: search_dir]
|
225
|
+
}
|
226
|
+
}.
|
227
|
+
flatten(1).
|
228
|
+
map {|args|
|
229
|
+
QB::Role.new *args
|
230
|
+
}.
|
231
|
+
uniq
|
232
|
+
end
|
233
|
+
|
234
|
+
# Get an array of QB::Role that match an input string.
|
235
|
+
#
|
236
|
+
# @return [Array<QB::Role>]
|
237
|
+
#
|
238
|
+
def self.matches input
|
239
|
+
# keep this here to we don't re-gen every loop
|
240
|
+
available = self.available
|
241
|
+
|
242
|
+
# first off, see if input matches any relative paths exactly
|
243
|
+
available.each {|role|
|
244
|
+
return [role] if role.display_path.to_s == input
|
245
|
+
}
|
246
|
+
|
247
|
+
# create an array of "separator" variations to try *exact* matching
|
248
|
+
# against. in order of preference:
|
249
|
+
#
|
250
|
+
# 1. exact input
|
251
|
+
# - this means if you ended up with roles that actually *are*
|
252
|
+
# differnetiated by '_/-' differences (which, IMHO, is a
|
253
|
+
# horrible fucking idea), you can get exactly what you ask for
|
254
|
+
# as a first priority
|
255
|
+
# 2. input with '-' changed to '_'
|
256
|
+
# - prioritized because convetion is to underscore-separate
|
257
|
+
# role names.
|
258
|
+
# 3. input with '_' changed to '-'
|
259
|
+
# - really just for convience's sake so you don't really have to
|
260
|
+
# remember what separator is used.
|
261
|
+
#
|
262
|
+
separator_variations = [
|
263
|
+
input,
|
264
|
+
input.gsub('-', '_'),
|
265
|
+
input.gsub('_', '-'),
|
266
|
+
]
|
267
|
+
|
268
|
+
separator_variations.each { |variation|
|
269
|
+
available.each { |role|
|
270
|
+
# exact match to full name
|
271
|
+
return [role] if role.name == variation
|
272
|
+
}.each { |role|
|
273
|
+
# exact match without the namespace prefix ('qb.' or similar)
|
274
|
+
return [role] if role.namespaceless == variation
|
275
|
+
}
|
276
|
+
}
|
277
|
+
|
278
|
+
# see if we prefix match any full names
|
279
|
+
separator_variations.each { |variation|
|
280
|
+
name_prefix_matches = available.select { |role|
|
281
|
+
role.name.start_with? variation
|
282
|
+
}
|
283
|
+
return name_prefix_matches unless name_prefix_matches.empty?
|
284
|
+
}
|
377
285
|
|
378
|
-
#
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
#
|
386
|
-
def self.to_display_path path
|
387
|
-
if path.realpath.start_with? QB::GEM_ROLES_DIR
|
388
|
-
path.realpath.sub (QB::GEM_ROLES_DIR.to_s + '/'), ''
|
389
|
-
else
|
390
|
-
QB::Util.contract_path path
|
286
|
+
# see if we prefix match any name
|
287
|
+
separator_variations.each { |variation|
|
288
|
+
namespaceless_prefix_matches = available.select { |role|
|
289
|
+
role.namespaceless.start_with? variation
|
290
|
+
}
|
291
|
+
unless namespaceless_prefix_matches.empty?
|
292
|
+
return namespaceless_prefix_matches
|
391
293
|
end
|
392
|
-
|
393
|
-
|
294
|
+
}
|
394
295
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
path.fnmatch? pathname.join('**')
|
296
|
+
# see if we prefix match any display paths
|
297
|
+
separator_variations.each { |variation|
|
298
|
+
path_prefix_matches = available.select { |role|
|
299
|
+
role.display_path.start_with? variation
|
400
300
|
}
|
301
|
+
return path_prefix_matches unless path_prefix_matches.empty?
|
302
|
+
}
|
303
|
+
|
304
|
+
# see if we word match any display paths
|
305
|
+
name_word_matches = available.select { |role|
|
306
|
+
QB::Util.words_start_with? role.display_path.to_s, input
|
307
|
+
}
|
308
|
+
return name_word_matches unless name_word_matches.empty?
|
309
|
+
|
310
|
+
# nada
|
311
|
+
[]
|
312
|
+
end # .matches
|
313
|
+
|
314
|
+
|
315
|
+
# Find exactly one matching role for the input string or raise.
|
316
|
+
#
|
317
|
+
# Where we look is determined by {QB::Role::PATH} via {QB::Role.search_path}.
|
318
|
+
#
|
319
|
+
# @param [String] input
|
320
|
+
# Input string term used to search (what we got off the CLI args).
|
321
|
+
#
|
322
|
+
# @return [QB::Role]
|
323
|
+
# The single matching role.
|
324
|
+
#
|
325
|
+
# @raise [QB::Role::NoMatchesError]
|
326
|
+
# If we didn't find any matches.
|
327
|
+
#
|
328
|
+
# @raise [QB::Role::MultipleMatchesError]
|
329
|
+
# If we matched more than one role.
|
330
|
+
#
|
331
|
+
def self.require input
|
332
|
+
as_pathname = Pathname.new(input)
|
401
333
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
#
|
406
|
-
# If it has 'roles' as a segment than use what's after that
|
407
|
-
#
|
408
|
-
split = path.to_s.split('/roles/')
|
409
|
-
|
410
|
-
end
|
334
|
+
# allow a path to a role dir
|
335
|
+
if role_dir? as_pathname
|
336
|
+
return QB::Role.new as_pathname
|
411
337
|
end
|
412
338
|
|
339
|
+
matches = self.matches input
|
413
340
|
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
# @!attribute [r] name
|
424
|
-
# @return [String]
|
425
|
-
# the role's ansible "name", which is it's directory name.
|
426
|
-
attr_reader :name
|
427
|
-
|
428
|
-
|
429
|
-
# @!attribute [r] display_path
|
430
|
-
#
|
431
|
-
# the path to the role that we display. we only show the directory name
|
432
|
-
# for QB roles, and use {QB::Util.compact_path} to show `.` and `~` for
|
433
|
-
# paths relative to the current directory and home directory, respectively.
|
434
|
-
#
|
435
|
-
# @return [Pathname]
|
436
|
-
attr_reader :display_path
|
437
|
-
|
438
|
-
|
439
|
-
# @!attribute [r] meta_path
|
440
|
-
# @return [String, nil]
|
441
|
-
# the path qb metadata was load from. `nil` if it's never been loaded
|
442
|
-
# or doesn't exist.
|
443
|
-
attr_reader :meta_path
|
444
|
-
|
445
|
-
|
446
|
-
# Constructor
|
447
|
-
# =======================================================================
|
448
|
-
|
449
|
-
# Instantiate a Role.
|
450
|
-
#
|
451
|
-
# @param [String|Pathname] path
|
452
|
-
# location of the role directory
|
453
|
-
#
|
454
|
-
# @param [nil, Pathname] search_dir
|
455
|
-
# Directory in {QB::Role.search_path} that the role was found in.
|
456
|
-
# Used to figure out it's name correctly when using directory-structure
|
457
|
-
# namespacing.
|
458
|
-
#
|
459
|
-
def initialize path, search_dir: nil
|
460
|
-
@path = if path.is_a?(Pathname) then path else Pathname.new(path) end
|
461
|
-
|
462
|
-
# check it...
|
463
|
-
unless @path.exist?
|
464
|
-
raise Errno::ENOENT.new @path.to_s
|
465
|
-
end
|
466
|
-
|
467
|
-
unless @path.directory?
|
468
|
-
raise Errno::ENOTDIR.new @path.to_s
|
469
|
-
end
|
470
|
-
|
471
|
-
@display_path = self.class.to_display_path @path
|
472
|
-
|
473
|
-
@meta_path = if (@path + 'meta' + 'qb').exist?
|
474
|
-
@path + 'meta' + 'qb'
|
475
|
-
elsif (@path + 'meta' + 'qb.yml').exist?
|
476
|
-
@path + 'meta' + 'qb.yml'
|
477
|
-
else
|
478
|
-
raise Errno::ENOENT.new "#{ @path.join('meta').to_s }/[qb|qb.yml]"
|
479
|
-
end
|
480
|
-
|
481
|
-
|
482
|
-
if search_dir.nil?
|
483
|
-
@name = @path.to_s.split(File::SEPARATOR).last
|
484
|
-
else
|
485
|
-
@name = @path.relative_path_from(search_dir).to_s
|
486
|
-
end
|
487
|
-
end # #initialize
|
488
|
-
|
341
|
+
role = case matches.length
|
342
|
+
when 0
|
343
|
+
raise QB::Role::NoMatchesError.new input
|
344
|
+
when 1
|
345
|
+
matches[0]
|
346
|
+
else
|
347
|
+
raise QB::Role::MultipleMatchesError.new input, matches
|
348
|
+
end
|
489
349
|
|
490
|
-
|
491
|
-
# =====================================================================
|
350
|
+
QB.debug "role match" => role
|
492
351
|
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
352
|
+
role
|
353
|
+
end # .require
|
354
|
+
|
355
|
+
|
356
|
+
# Get the include path for an included role based on the
|
357
|
+
# option metadata that defines the include and the current
|
358
|
+
# include path.
|
359
|
+
#
|
360
|
+
# @param role [Role]
|
361
|
+
# the role to include.
|
362
|
+
#
|
363
|
+
# @param option_meta [Hash]
|
364
|
+
# the entry for the option in qb.yml
|
365
|
+
#
|
366
|
+
# @param current_include_path [Array<string>]
|
367
|
+
#
|
368
|
+
# @return [Array<string>]
|
369
|
+
# include path for the included role.
|
370
|
+
#
|
371
|
+
def self.get_include_path role, option_meta, current_include_path
|
372
|
+
new_include_path = if option_meta.key? 'as'
|
373
|
+
case option_meta['as']
|
374
|
+
when nil, false
|
375
|
+
# include it in with the parent role's options
|
376
|
+
current_include_path
|
377
|
+
when String
|
378
|
+
current_include_path + [option_meta['as']]
|
500
379
|
else
|
501
|
-
|
380
|
+
raise QB::Role::MetadataError.new,
|
381
|
+
"bad 'as' value: #{ option_meta.inspect }"
|
502
382
|
end
|
383
|
+
else
|
384
|
+
current_include_path + [role.namespaceless]
|
503
385
|
end
|
386
|
+
end
|
387
|
+
|
388
|
+
|
389
|
+
# The path we display in the CLI, see {#display_path}.
|
390
|
+
#
|
391
|
+
# @param [Pathname | String] path
|
392
|
+
# input path to transform.
|
393
|
+
#
|
394
|
+
# @return [Pathname]
|
395
|
+
# path to display.
|
396
|
+
#
|
397
|
+
def self.to_display_path path
|
398
|
+
if path.realpath.start_with? QB::GEM_ROLES_DIR
|
399
|
+
path.realpath.sub (QB::GEM_ROLES_DIR.to_s + '/'), ''
|
400
|
+
else
|
401
|
+
QB::Util.contract_path path
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
# @todo This was gonna be something at some point, but didn't get done.
|
407
|
+
#
|
408
|
+
def self.guess_role_name dest
|
409
|
+
raise "TODO: Not implemented!!!"
|
504
410
|
|
505
|
-
|
506
|
-
File.basename(@name).split('.', 2).last
|
507
|
-
end
|
411
|
+
path = QB::Util.resolve dest
|
508
412
|
|
509
|
-
|
510
|
-
|
511
|
-
|
413
|
+
search_dirs = search_path.find_all { |pathname|
|
414
|
+
path.fnmatch? pathname.join('**')
|
415
|
+
}
|
512
416
|
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
417
|
+
case search_dirs.length
|
418
|
+
when 0
|
419
|
+
# It's not in any of the search directories
|
420
|
+
#
|
421
|
+
# If it has 'roles' as a segment than use what's after that
|
422
|
+
#
|
423
|
+
split = path.to_s.split('/roles/')
|
424
|
+
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
|
429
|
+
# Instance Attributes
|
430
|
+
# =======================================================================
|
431
|
+
|
432
|
+
# @!attribute [r] path
|
433
|
+
# @return [Pathname]
|
434
|
+
# location of the role directory.
|
435
|
+
attr_reader :path
|
436
|
+
|
437
|
+
|
438
|
+
# @!attribute [r] name
|
439
|
+
# @return [String]
|
440
|
+
# the role's ansible "name", which is it's directory name.
|
441
|
+
attr_reader :name
|
442
|
+
|
443
|
+
|
444
|
+
# @!attribute [r] display_path
|
445
|
+
#
|
446
|
+
# the path to the role that we display. we only show the directory name
|
447
|
+
# for QB roles, and use {QB::Util.compact_path} to show `.` and `~` for
|
448
|
+
# paths relative to the current directory and home directory, respectively.
|
449
|
+
#
|
450
|
+
# @return [Pathname]
|
451
|
+
attr_reader :display_path
|
452
|
+
|
453
|
+
|
454
|
+
# @!attribute [r] meta_path
|
455
|
+
# @return [String, nil]
|
456
|
+
# the path qb metadata was load from. `nil` if it's never been loaded
|
457
|
+
# or doesn't exist.
|
458
|
+
attr_reader :meta_path
|
459
|
+
|
460
|
+
|
461
|
+
# Constructor
|
462
|
+
# =======================================================================
|
463
|
+
|
464
|
+
# Instantiate a Role.
|
465
|
+
#
|
466
|
+
# @param [String|Pathname] path
|
467
|
+
# location of the role directory
|
468
|
+
#
|
469
|
+
# @param [nil, Pathname] search_dir
|
470
|
+
# Directory in {QB::Role.search_path} that the role was found in.
|
471
|
+
# Used to figure out it's name correctly when using directory-structure
|
472
|
+
# namespacing.
|
473
|
+
#
|
474
|
+
def initialize path, search_dir: nil
|
475
|
+
@path = if path.is_a?(Pathname) then path else Pathname.new(path) end
|
476
|
+
|
477
|
+
# check it...
|
478
|
+
unless @path.exist?
|
479
|
+
raise Errno::ENOENT.new @path.to_s
|
480
|
+
end
|
481
|
+
|
482
|
+
unless @path.directory?
|
483
|
+
raise Errno::ENOTDIR.new @path.to_s
|
484
|
+
end
|
485
|
+
|
486
|
+
@display_path = self.class.to_display_path @path
|
487
|
+
|
488
|
+
@meta_path = if (@path + 'meta' + 'qb').exist?
|
489
|
+
@path + 'meta' + 'qb'
|
490
|
+
elsif (@path + 'meta' + 'qb.yml').exist?
|
491
|
+
@path + 'meta' + 'qb.yml'
|
492
|
+
else
|
493
|
+
raise Errno::ENOENT.new "#{ @path.join('meta').to_s }/[qb|qb.yml]"
|
494
|
+
end
|
495
|
+
|
496
|
+
|
497
|
+
if search_dir.nil?
|
498
|
+
@name = @path.to_s.split(File::SEPARATOR).last
|
499
|
+
else
|
500
|
+
@name = @path.relative_path_from(search_dir).to_s
|
501
|
+
end
|
502
|
+
end # #initialize
|
503
|
+
|
504
|
+
|
505
|
+
# Instance Methods
|
506
|
+
# =====================================================================
|
507
|
+
|
508
|
+
def namespace
|
509
|
+
*namespace_segments, last = @name.split File::Separator
|
510
|
+
|
511
|
+
namespace_segments << last.split('.').first if last.include?('.')
|
512
|
+
|
513
|
+
if namespace_segments.empty?
|
514
|
+
nil
|
515
|
+
else
|
516
|
+
File.join *namespace_segments
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
def namespaceless
|
521
|
+
File.basename(@name).split('.', 2).last
|
522
|
+
end
|
523
|
+
|
524
|
+
def options_key
|
525
|
+
@display_path.to_s
|
526
|
+
end
|
527
|
+
|
528
|
+
# load qb metadata from meta/qb.yml or from executing meta/qb and parsing
|
529
|
+
# the YAML written to stdout.
|
530
|
+
#
|
531
|
+
# if `cache` is true caches it as `@meta`
|
532
|
+
#
|
533
|
+
def load_meta cache = true
|
534
|
+
meta = if @meta_path.extname == '.yml'
|
535
|
+
contents = begin
|
536
|
+
@meta_path.read
|
537
|
+
rescue Exception => error
|
538
|
+
raise QB::Role::MetadataError,
|
539
|
+
"Failed to read metadata file at #{ @meta_path.to_s }, " +
|
540
|
+
"error: #{ error.inspect }"
|
537
541
|
end
|
538
542
|
|
539
|
-
|
540
|
-
|
543
|
+
begin
|
544
|
+
YAML.load(contents) || {}
|
545
|
+
rescue Exception => error
|
546
|
+
raise QB::Role::MetadataError,
|
547
|
+
"Failed to load metadata YAML from #{ @meta_path.to_s }, " +
|
548
|
+
"error: #{ error.inspect }"
|
541
549
|
end
|
542
|
-
|
543
|
-
|
550
|
+
else
|
551
|
+
YAML.load(Cmds.out!(@meta_path.realpath.to_s)) || {}
|
544
552
|
end
|
545
553
|
|
546
|
-
|
547
|
-
|
548
|
-
@meta || load_meta
|
554
|
+
if cache
|
555
|
+
@meta = meta
|
549
556
|
end
|
550
557
|
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
#
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
558
|
+
meta
|
559
|
+
end
|
560
|
+
|
561
|
+
# @return [Hash{String => Object}]
|
562
|
+
# the QB metadata for the role.
|
563
|
+
#
|
564
|
+
def meta
|
565
|
+
@meta || load_meta
|
566
|
+
end
|
567
|
+
|
568
|
+
|
569
|
+
# gets the variable prefix that will be appended to cli options before
|
570
|
+
# passing them to the role. defaults to `#namespaceless` unless specified
|
571
|
+
# in meta.
|
572
|
+
def var_prefix
|
573
|
+
# ugh, i was generating meta/qb.yml files that set 'var_prefix' to
|
574
|
+
# `null`, but it would be nice to
|
575
|
+
#
|
576
|
+
meta_or 'var_prefix', namespaceless
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
# get the options from the metadata, defaulting to [] if none defined
|
581
|
+
def option_metas
|
582
|
+
meta_or ['options', 'opts', 'vars'], []
|
583
|
+
end
|
584
|
+
|
585
|
+
|
586
|
+
# @return [Array<QB::Options::Option>
|
587
|
+
# an array of Option for the role, including any included roles.
|
588
|
+
#
|
589
|
+
def options include_path = []
|
590
|
+
option_metas.map {|option_meta|
|
591
|
+
if option_meta.key? 'include'
|
592
|
+
role_name = option_meta['include']
|
593
|
+
role = QB::Role.require role_name
|
594
|
+
role.options Role.get_include_path(role, option_meta, include_path)
|
585
595
|
else
|
586
|
-
|
596
|
+
QB::Options::Option.new self, option_meta, include_path
|
587
597
|
end
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
598
|
+
}.flatten
|
599
|
+
end # #options
|
600
|
+
|
601
|
+
|
602
|
+
# loads the defaults from vars/main.yml and defaults/main.yml,
|
603
|
+
# caching by default. vars override defaults values.
|
604
|
+
def load_defaults cache = true
|
605
|
+
defaults_path = @path + 'defaults' + 'main.yml'
|
606
|
+
defaults = if defaults_path.file?
|
607
|
+
YAML.load(defaults_path.read) || {}
|
608
|
+
else
|
609
|
+
{}
|
610
|
+
end
|
611
|
+
|
612
|
+
vars_path = @path + 'vars' + 'main.yml'
|
613
|
+
vars = if vars_path.file?
|
614
|
+
YAML.load(vars_path.read) || {}
|
615
|
+
else
|
616
|
+
{}
|
617
|
+
end
|
618
|
+
|
619
|
+
defaults = defaults.merge! vars
|
620
|
+
|
621
|
+
if cache
|
622
|
+
@defaults = defaults
|
623
|
+
end
|
624
|
+
|
625
|
+
defaults
|
626
|
+
end
|
627
|
+
|
628
|
+
# gets the role variable defaults from defaults/main.yml, or {}
|
629
|
+
def defaults
|
630
|
+
@defaults || load_defaults
|
631
|
+
end
|
632
|
+
|
633
|
+
def save_options
|
634
|
+
!!meta_or('save_options', true)
|
635
|
+
end
|
636
|
+
|
637
|
+
# if the exe should auto-make the directory. this is nice for most roles
|
638
|
+
# but some need it to be missing
|
639
|
+
def mkdir
|
640
|
+
!!meta_or('mkdir', true)
|
641
|
+
end
|
642
|
+
|
643
|
+
# @return [String]
|
644
|
+
# usage information formatted as plain text for the CLI.
|
645
|
+
#
|
646
|
+
def usage
|
647
|
+
# Split up options by required and optional.
|
648
|
+
required_options = []
|
649
|
+
optional_options = []
|
650
|
+
|
651
|
+
options.each { |option|
|
652
|
+
if option.required?
|
653
|
+
required_options << option
|
592
654
|
else
|
593
|
-
|
655
|
+
optional_options << option
|
594
656
|
end
|
595
|
-
|
596
|
-
defaults = defaults.merge! vars
|
597
|
-
|
598
|
-
if cache
|
599
|
-
@defaults = defaults
|
600
|
-
end
|
601
|
-
|
602
|
-
defaults
|
603
|
-
end
|
657
|
+
}
|
604
658
|
|
605
|
-
|
606
|
-
def defaults
|
607
|
-
@defaults || load_defaults
|
608
|
-
end
|
659
|
+
parts = ['qb [run]', name]
|
609
660
|
|
610
|
-
|
611
|
-
|
612
|
-
|
661
|
+
required_options.each { |option|
|
662
|
+
parts << option.usage
|
663
|
+
}
|
613
664
|
|
614
|
-
|
615
|
-
|
616
|
-
def mkdir
|
617
|
-
!!meta_or('mkdir', true)
|
665
|
+
unless optional_options.empty?
|
666
|
+
parts << '[OPTIONS]'
|
618
667
|
end
|
619
668
|
|
620
|
-
|
621
|
-
parts
|
622
|
-
options.each {|option|
|
623
|
-
if option.required?
|
624
|
-
parts << option.usage
|
625
|
-
end
|
626
|
-
}
|
627
|
-
parts << '[OPTIONS] DIRECTORY'
|
628
|
-
parts.join(' ')
|
669
|
+
if uses_default_dir?
|
670
|
+
parts << 'DIRECTORY'
|
629
671
|
end
|
630
672
|
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
lines << ''
|
639
|
-
if meta['description']
|
640
|
-
lines << meta['description']
|
641
|
-
lines << ''
|
642
|
-
end
|
643
|
-
lines << 'usage:'
|
644
|
-
# lines << " qb #{ name } [OPTIONS] DIRECTORY"
|
645
|
-
lines << " #{ usage }"
|
646
|
-
lines << ''
|
647
|
-
lines << 'options:'
|
648
|
-
|
649
|
-
lines.join("\n")
|
650
|
-
end
|
651
|
-
|
652
|
-
def examples
|
653
|
-
@meta['examples']
|
654
|
-
end
|
655
|
-
|
656
|
-
# format the `meta.examples` hash into a string suitable for cli
|
657
|
-
# output.
|
658
|
-
#
|
659
|
-
# @return [String]
|
660
|
-
# the CLI-formatted examples.
|
661
|
-
#
|
662
|
-
def format_examples
|
663
|
-
examples.
|
664
|
-
map {|title, body|
|
665
|
-
[
|
666
|
-
"#{ title }:",
|
667
|
-
body.lines.map {|l|
|
668
|
-
# only indent non-empty lines
|
669
|
-
# makes compacting newline sequences easier (see below)
|
670
|
-
if l.match(/^\s*$/)
|
671
|
-
l
|
672
|
-
else
|
673
|
-
' ' + l
|
674
|
-
end
|
675
|
-
},
|
676
|
-
''
|
677
|
-
]
|
678
|
-
}.
|
679
|
-
flatten.
|
680
|
-
join("\n").
|
681
|
-
# compact newline sequences
|
682
|
-
gsub(/\n\n+/, "\n\n")
|
683
|
-
end
|
673
|
+
parts.join ' '
|
674
|
+
end
|
675
|
+
|
676
|
+
|
677
|
+
# get the CLI banner for the role
|
678
|
+
def banner
|
679
|
+
lines = []
|
684
680
|
|
685
|
-
#
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
# should qb ask for an ansible vault password?
|
693
|
-
#
|
694
|
-
# @see http://docs.ansible.com/ansible/playbooks_vault.html
|
695
|
-
#
|
696
|
-
# @return [Boolean]
|
697
|
-
# `true` if qb should ask for a vault password.
|
698
|
-
#
|
699
|
-
def ask_vault_pass?
|
700
|
-
!!@meta['ask_vault_pass']
|
681
|
+
name_line = "#{ name } role"
|
682
|
+
lines << name_line
|
683
|
+
lines << "=" * name_line.length
|
684
|
+
lines << ''
|
685
|
+
if meta['description']
|
686
|
+
lines << meta['description']
|
687
|
+
lines << ''
|
701
688
|
end
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
689
|
+
lines << 'usage:'
|
690
|
+
lines << " #{ usage }"
|
691
|
+
lines << ''
|
692
|
+
lines << 'options:'
|
693
|
+
|
694
|
+
lines.join("\n")
|
695
|
+
end
|
696
|
+
|
697
|
+
|
698
|
+
def examples
|
699
|
+
@meta['examples']
|
700
|
+
end
|
701
|
+
|
702
|
+
# format the `meta.examples` hash into a string suitable for cli
|
703
|
+
# output.
|
704
|
+
#
|
705
|
+
# @return [String]
|
706
|
+
# the CLI-formatted examples.
|
707
|
+
#
|
708
|
+
def format_examples
|
709
|
+
examples.
|
710
|
+
map {|title, body|
|
711
|
+
[
|
712
|
+
"#{ title }:",
|
713
|
+
body.lines.map {|l|
|
714
|
+
# only indent non-empty lines
|
715
|
+
# makes compacting newline sequences easier (see below)
|
716
|
+
if l.match(/^\s*$/)
|
717
|
+
l
|
718
|
+
else
|
719
|
+
' ' + l
|
720
|
+
end
|
721
|
+
},
|
722
|
+
''
|
723
|
+
]
|
724
|
+
}.
|
725
|
+
flatten.
|
726
|
+
join("\n").
|
727
|
+
# compact newline sequences
|
728
|
+
gsub(/\n\n+/, "\n\n")
|
729
|
+
end
|
730
|
+
|
731
|
+
# examples text
|
732
|
+
def puts_examples
|
733
|
+
return unless examples
|
734
|
+
|
735
|
+
puts "\n" + format_examples + "\n"
|
736
|
+
end
|
737
|
+
|
738
|
+
# should qb ask for an ansible vault password?
|
739
|
+
#
|
740
|
+
# @see http://docs.ansible.com/ansible/playbooks_vault.html
|
741
|
+
#
|
742
|
+
# @return [Boolean]
|
743
|
+
# `true` if qb should ask for a vault password.
|
744
|
+
#
|
745
|
+
def ask_vault_pass?
|
746
|
+
!!@meta['ask_vault_pass']
|
747
|
+
end
|
748
|
+
|
749
|
+
|
750
|
+
# @return [Boolean]
|
751
|
+
# @todo Document return value.
|
752
|
+
#
|
753
|
+
def uses_default_dir?
|
754
|
+
meta['default_dir'] != false
|
755
|
+
end # #uses_default_dir?
|
756
|
+
|
757
|
+
|
758
|
+
# gets the default `qb_dir` value, raising an error if the role doesn't
|
759
|
+
# define how to get one or there is a problem getting it.
|
760
|
+
#
|
761
|
+
#
|
762
|
+
def default_dir cwd, options
|
763
|
+
QB.debug "get_default_dir",
|
764
|
+
role: self,
|
765
|
+
meta: self.meta,
|
766
|
+
cwd: cwd,
|
767
|
+
options: options
|
768
|
+
|
769
|
+
key = 'default_dir'
|
770
|
+
value = self.meta[key]
|
771
|
+
case value
|
772
|
+
when nil
|
773
|
+
# there is no get_dir info in meta/qb.yml, can't get the dir
|
774
|
+
raise QB::UserInputError.dedented <<-END
|
775
|
+
unable to infer default directory: no '#{ key }' key in 'meta/qb.yml'
|
776
|
+
for role #{ self }
|
777
|
+
END
|
778
|
+
|
779
|
+
when false
|
780
|
+
# this method should not get called when the value is false (an entire
|
781
|
+
# section is skipped in exe/qb when `default_dir = false`)
|
782
|
+
raise QB::StateError.squished <<-END
|
783
|
+
role does not use default directory (meta/qb.yml:default_dir = false)
|
784
|
+
END
|
785
|
+
|
786
|
+
when 'git_root'
|
787
|
+
QB.debug "returning the git root relative to cwd"
|
788
|
+
NRSER.git_root cwd
|
789
|
+
|
790
|
+
when 'cwd'
|
791
|
+
QB.debug "returning current working directory"
|
792
|
+
cwd
|
793
|
+
|
794
|
+
when Hash
|
795
|
+
QB.debug "qb meta option is a Hash"
|
796
|
+
|
797
|
+
unless value.length == 1
|
798
|
+
raise "#{ meta_path.to_s }:default_dir invalid: #{ value.inspect }"
|
799
|
+
end
|
729
800
|
|
730
|
-
|
731
|
-
QB.debug "returning the git root relative to cwd"
|
732
|
-
NRSER.git_root cwd
|
801
|
+
hash_key, hash_value = value.first
|
733
802
|
|
734
|
-
|
735
|
-
|
736
|
-
|
803
|
+
case hash_key
|
804
|
+
when 'exe'
|
805
|
+
exe_path = hash_value
|
737
806
|
|
738
|
-
|
739
|
-
|
807
|
+
# supply the options to the exe so it can make work off those values
|
808
|
+
# if it wants.
|
809
|
+
exe_input_data = Hash[
|
810
|
+
options.map {|option|
|
811
|
+
[option.cli_option_name, option.value]
|
812
|
+
}
|
813
|
+
]
|
740
814
|
|
741
|
-
unless
|
742
|
-
|
815
|
+
unless exe_path.start_with?('~') || exe_path.start_with?('/')
|
816
|
+
exe_path = File.join(self.path, exe_path)
|
817
|
+
debug 'exe path is relative, basing off role dir', exe_path: exe_path
|
743
818
|
end
|
744
819
|
|
745
|
-
|
820
|
+
debug "found 'exe' key, calling", exe_path: exe_path,
|
821
|
+
exe_input_data: exe_input_data
|
746
822
|
|
747
|
-
|
748
|
-
|
749
|
-
exe_path = hash_value
|
750
|
-
|
751
|
-
# supply the options to the exe so it can make work off those values
|
752
|
-
# if it wants.
|
753
|
-
exe_input_data = Hash[
|
754
|
-
options.map {|option|
|
755
|
-
[option.cli_option_name, option.value]
|
756
|
-
}
|
757
|
-
]
|
758
|
-
|
759
|
-
unless exe_path.start_with?('~') || exe_path.start_with?('/')
|
760
|
-
exe_path = File.join(self.path, exe_path)
|
761
|
-
debug 'exe path is relative, basing off role dir', exe_path: exe_path
|
762
|
-
end
|
763
|
-
|
764
|
-
debug "found 'exe' key, calling", exe_path: exe_path,
|
765
|
-
exe_input_data: exe_input_data
|
766
|
-
|
767
|
-
Cmds.chomp! exe_path do
|
768
|
-
JSON.dump exe_input_data
|
769
|
-
end
|
770
|
-
|
771
|
-
when 'find_up'
|
772
|
-
filename = hash_value
|
773
|
-
|
774
|
-
unless filename.is_a? String
|
775
|
-
raise "find_up filename must be string, found #{ filename.inspect }"
|
776
|
-
end
|
777
|
-
|
778
|
-
QB.debug "found 'find_up', looking for file named #{ filename }"
|
779
|
-
|
780
|
-
QB::Util.find_up filename
|
781
|
-
|
782
|
-
else
|
783
|
-
raise QB::Role::MetadataError.squised <<-END
|
784
|
-
bad key: #{ hash_key } in #{ self.meta_path.to_s }:default_dir
|
785
|
-
END
|
786
|
-
|
823
|
+
Cmds.chomp! exe_path do
|
824
|
+
JSON.dump exe_input_data
|
787
825
|
end
|
826
|
+
|
827
|
+
when 'find_up'
|
828
|
+
filename = hash_value
|
829
|
+
|
830
|
+
unless filename.is_a? String
|
831
|
+
raise "find_up filename must be string, found #{ filename.inspect }"
|
832
|
+
end
|
833
|
+
|
834
|
+
QB.debug "found 'find_up', looking for file named #{ filename }"
|
835
|
+
|
836
|
+
QB::Util.find_up filename
|
837
|
+
|
838
|
+
else
|
839
|
+
raise QB::Role::MetadataError.squised <<-END
|
840
|
+
bad key: #{ hash_key } in #{ self.meta_path.to_s }:default_dir
|
841
|
+
END
|
842
|
+
|
788
843
|
end
|
789
|
-
end # default_dir
|
790
|
-
|
791
|
-
# @return [Hash<String, *>]
|
792
|
-
# default `ansible-playbook` CLI options from role qb metadata.
|
793
|
-
# Hash of option name to value.
|
794
|
-
def default_ansible_options
|
795
|
-
meta_or 'ansible_options', {}
|
796
844
|
end
|
845
|
+
end # default_dir
|
846
|
+
|
847
|
+
|
848
|
+
# @return [Hash<String, *>]
|
849
|
+
# default `ansible-playbook` CLI options from role qb metadata.
|
850
|
+
# Hash of option name to value.
|
851
|
+
def default_ansible_options
|
852
|
+
meta_or 'ansible_options', {}
|
853
|
+
end
|
854
|
+
|
855
|
+
|
856
|
+
# Get the {Gem::Requirement} parse of the `qb_requirement` key in
|
857
|
+
# {#meta} (if it is defined), which specifies the required version of
|
858
|
+
# `qb` for the role.
|
859
|
+
#
|
860
|
+
# @return [Gem::Requirement, nil]
|
861
|
+
# The requirement if `required_qb_version` key is in {#meta}, else `nil`.
|
862
|
+
#
|
863
|
+
def qb_requirement
|
864
|
+
if meta['requirements'] &&
|
865
|
+
meta['requirements']['gems'] &&
|
866
|
+
meta['requirements']['gems']['qb']
|
867
|
+
Gem::Requirement.new meta['requirements']['gems']['qb']
|
868
|
+
end
|
869
|
+
end
|
870
|
+
|
871
|
+
|
872
|
+
# Language Inter-Op
|
873
|
+
# -----------------------------------------------------------------------
|
874
|
+
|
875
|
+
def hash
|
876
|
+
path.realpath.hash
|
877
|
+
end
|
878
|
+
|
879
|
+
|
880
|
+
def == other
|
881
|
+
other.is_a?(self.class) && other.path.realpath == path.realpath
|
882
|
+
end
|
883
|
+
|
884
|
+
alias_method :eql?, :==
|
885
|
+
|
886
|
+
|
887
|
+
# @return [String]
|
888
|
+
# {QB::Role#display_path}
|
889
|
+
#
|
890
|
+
def to_s
|
891
|
+
@display_path.to_s
|
892
|
+
end
|
893
|
+
|
894
|
+
|
895
|
+
private
|
896
|
+
# -----------------------------------------------------------------------
|
897
|
+
|
898
|
+
# get the value at the first found of the keys or the default.
|
899
|
+
#
|
900
|
+
# `nil` (`null` in yaml files) are treated like they're not there at
|
901
|
+
# all. you need to use `false` if you want to tell QB not to do something.
|
902
|
+
#
|
903
|
+
# @param [String | Symbol | Array<String | Symbol>] keys
|
904
|
+
# Single
|
905
|
+
#
|
906
|
+
# @return [Object]
|
907
|
+
#
|
908
|
+
def meta_or keys, default
|
909
|
+
keys.as_array.map(&:to_s).each do |key|
|
910
|
+
return meta[key] unless meta[key].nil?
|
911
|
+
end
|
912
|
+
|
913
|
+
# We didn't find anything (that wasn't explicitly or implicitly `nil`)
|
914
|
+
default
|
915
|
+
end # meta_or
|
797
916
|
|
798
917
|
|
799
|
-
#
|
800
|
-
# {#meta} (if it is defined), which specifies the required version of
|
801
|
-
# `qb` for the role.
|
918
|
+
# Find a non-null/nil value for one of `keys` or raise an error.
|
802
919
|
#
|
803
|
-
# @
|
804
|
-
#
|
920
|
+
# @param [String | Symbol | Array<String | Symbol>] keys
|
921
|
+
# Possible key names. They will be searched in order and first
|
922
|
+
# non-null/nil value returned.
|
805
923
|
#
|
806
|
-
|
807
|
-
if meta['requirements'] &&
|
808
|
-
meta['requirements']['gems'] &&
|
809
|
-
meta['requirements']['gems']['qb']
|
810
|
-
Gem::Requirement.new meta['requirements']['gems']['qb']
|
811
|
-
end
|
812
|
-
end
|
813
|
-
|
814
|
-
|
815
|
-
# language inter-op
|
816
|
-
# -----------------------------------------------------------------------
|
817
|
-
|
818
|
-
def hash
|
819
|
-
path.realpath.hash
|
820
|
-
end
|
821
|
-
|
822
|
-
|
823
|
-
def == other
|
824
|
-
other.is_a?(Role) && other.path.realpath == path.realpath
|
825
|
-
end
|
826
|
-
|
827
|
-
alias_method :eql?, :==
|
828
|
-
|
829
|
-
# @return [String]
|
830
|
-
# {QB::Role#display_path}
|
924
|
+
# @return [Object]
|
831
925
|
#
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
926
|
+
# @raise [QB::Role::MetadataError]
|
927
|
+
# If none of `keys` are found.
|
928
|
+
#
|
929
|
+
def need_meta keys
|
930
|
+
keys = keys.as_array.map(&:to_s)
|
931
|
+
|
932
|
+
keys.each do |key|
|
933
|
+
return meta[key] unless meta[key].nil?
|
934
|
+
end
|
935
|
+
|
936
|
+
raise QB::Role::MetadataError.squished <<-END
|
937
|
+
Expected metadata for role #{ self } to define (non-null) value for
|
938
|
+
one of keys #{ keys.inspect }.
|
939
|
+
END
|
940
|
+
end # need_meta
|
836
941
|
|
837
|
-
private
|
838
|
-
# -----------------------------------------------------------------------
|
839
942
|
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
# all. you need to use `false` if you want to tell QB not to do something.
|
844
|
-
#
|
845
|
-
def meta_or keys, default
|
846
|
-
keys = [keys] if keys.is_a? String
|
847
|
-
keys.each do |key|
|
848
|
-
if meta.key?(key) && !meta[key].nil?
|
849
|
-
return meta[key]
|
850
|
-
end
|
851
|
-
end
|
852
|
-
default
|
853
|
-
end # meta_or
|
854
|
-
|
855
|
-
# end private
|
856
|
-
end # Role
|
857
|
-
end # QB
|
943
|
+
# end private
|
944
|
+
|
945
|
+
end # class QB::Role
|