cloud-mu 2.0.4 → 2.1.0beta

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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +6 -0
  3. data/ansible/roles/geerlingguy.firewall/LICENSE +20 -0
  4. data/ansible/roles/geerlingguy.firewall/README.md +93 -0
  5. data/ansible/roles/geerlingguy.firewall/defaults/main.yml +19 -0
  6. data/ansible/roles/geerlingguy.firewall/handlers/main.yml +3 -0
  7. data/ansible/roles/geerlingguy.firewall/meta/main.yml +26 -0
  8. data/ansible/roles/geerlingguy.firewall/molecule/default/molecule.yml +40 -0
  9. data/ansible/roles/geerlingguy.firewall/molecule/default/playbook.yml +17 -0
  10. data/ansible/roles/geerlingguy.firewall/molecule/default/tests/test_default.py +14 -0
  11. data/ansible/roles/geerlingguy.firewall/molecule/default/yaml-lint.yml +6 -0
  12. data/ansible/roles/geerlingguy.firewall/tasks/disable-other-firewalls.yml +66 -0
  13. data/ansible/roles/geerlingguy.firewall/tasks/main.yml +44 -0
  14. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +136 -0
  15. data/ansible/roles/geerlingguy.firewall/templates/firewall.init.j2 +52 -0
  16. data/ansible/roles/geerlingguy.firewall/templates/firewall.unit.j2 +12 -0
  17. data/bin/mu-ansible-secret +114 -0
  18. data/bin/mu-aws-setup +74 -21
  19. data/bin/mu-node-manage +22 -12
  20. data/bin/mu-self-update +11 -4
  21. data/cloud-mu.gemspec +3 -3
  22. data/cookbooks/firewall/metadata.json +1 -1
  23. data/cookbooks/firewall/recipes/default.rb +4 -0
  24. data/cookbooks/mu-master/recipes/default.rb +0 -3
  25. data/cookbooks/mu-master/recipes/init.rb +15 -9
  26. data/cookbooks/mu-master/templates/default/mu.rc.erb +1 -1
  27. data/cookbooks/mu-master/templates/default/web_app.conf.erb +0 -4
  28. data/cookbooks/mu-php54/metadata.rb +2 -2
  29. data/cookbooks/mu-php54/recipes/default.rb +1 -3
  30. data/cookbooks/mu-tools/recipes/eks.rb +25 -2
  31. data/cookbooks/mu-tools/recipes/nrpe.rb +6 -1
  32. data/cookbooks/mu-tools/recipes/set_mu_hostname.rb +8 -0
  33. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  34. data/cookbooks/mu-tools/templates/default/kubeconfig.erb +2 -2
  35. data/cookbooks/mu-tools/templates/default/kubelet-config.json.erb +35 -0
  36. data/extras/clean-stock-amis +10 -4
  37. data/extras/list-stock-amis +64 -0
  38. data/extras/python_rpm/build.sh +21 -0
  39. data/extras/python_rpm/muthon.spec +68 -0
  40. data/install/README.md +5 -2
  41. data/install/user-dot-murc.erb +1 -1
  42. data/modules/mu.rb +52 -8
  43. data/modules/mu/clouds/aws.rb +1 -1
  44. data/modules/mu/clouds/aws/container_cluster.rb +1071 -47
  45. data/modules/mu/clouds/aws/firewall_rule.rb +45 -19
  46. data/modules/mu/clouds/aws/log.rb +3 -2
  47. data/modules/mu/clouds/aws/role.rb +18 -2
  48. data/modules/mu/clouds/aws/server.rb +11 -5
  49. data/modules/mu/clouds/aws/server_pool.rb +20 -24
  50. data/modules/mu/clouds/aws/userdata/linux.erb +1 -1
  51. data/modules/mu/clouds/aws/vpc.rb +9 -0
  52. data/modules/mu/clouds/google/server.rb +2 -0
  53. data/modules/mu/config.rb +3 -3
  54. data/modules/mu/config/container_cluster.rb +1 -1
  55. data/modules/mu/config/firewall_rule.rb +4 -0
  56. data/modules/mu/config/role.rb +29 -0
  57. data/modules/mu/config/server.rb +9 -4
  58. data/modules/mu/groomer.rb +14 -3
  59. data/modules/mu/groomers/ansible.rb +553 -0
  60. data/modules/mu/groomers/chef.rb +0 -5
  61. data/modules/mu/mommacat.rb +18 -3
  62. data/modules/scratchpad.erb +1 -1
  63. data/requirements.txt +5 -0
  64. metadata +39 -16
@@ -132,9 +132,14 @@ module MU
132
132
  "description" => "Bootstrap asynchronously via the Momma Cat daemon instead of during the main deployment process"
133
133
  },
134
134
  "groomer" => {
135
- "type" => "string",
136
- "default" => MU::Config.defaultGroomer,
137
- "enum" => MU.supportedGroomers
135
+ "type" => "string",
136
+ "default" => MU::Config.defaultGroomer,
137
+ "enum" => MU.supportedGroomers
138
+ },
139
+ "groomer_autofetch" => {
140
+ "type" => "boolean",
141
+ "description" => "For groomer implementations which support automatically fetching roles/recipes/manifests from a public library, such as Ansible Galaxy, this will toggle this behavior on or off.",
142
+ "default" => true
138
143
  },
139
144
  "groom" => {
140
145
  "type" => "boolean",
@@ -415,7 +420,7 @@ module MU
415
420
  "type" => "array",
416
421
  "items" => {
417
422
  "type" => "string",
418
- "description" => "Chef run list entry, e.g. role[rolename] or recipe[recipename]."
423
+ "description" => "A list of +groomer+ recipes/roles/scripts to run, using naming conventions specific to the appropriate grooming layer. In +Chef+, this corresponds to a node's +run_list+ attribute, and entries should be of the form <tt>role[rolename]</tt> or <tt>recipe[recipename]</tt>. In +Ansible+, it should be a list of roles (+rolename+), which Mu will use to generate a custom Playbook for the deployment."
419
424
  }
420
425
  },
421
426
  "ingress_rules" => {
@@ -18,12 +18,16 @@ module MU
18
18
  class Groomer
19
19
 
20
20
  # An exception denoting a Groomer run that has failed
21
- class RunError < MuError;
21
+ class RunError < MuError
22
+ end
23
+
24
+ # An exception denoting nonexistent secret
25
+ class MuNoSuchSecret < StandardError
22
26
  end
23
27
 
24
28
  # List of known/supported grooming agents (configuration management tools)
25
29
  def self.supportedGroomers
26
- ["Chef"]
30
+ ["Chef", "Ansible"]
27
31
  end
28
32
 
29
33
  # Instance methods that any Groomer plugin must implement
@@ -36,17 +40,24 @@ module MU
36
40
  [:getSecret, :cleanup, :saveSecret, :deleteSecret]
37
41
  end
38
42
 
43
+ class Ansible;
44
+ end
39
45
 
40
46
  class Chef;
41
47
  end
48
+
42
49
  # @param groomer [String]: The grooming agent to load.
43
50
  # @return [Class]: The class object implementing this groomer agent
44
51
  def self.loadGroomer(groomer)
45
52
  if !File.size?(MU.myRoot+"/modules/mu/groomers/#{groomer.downcase}.rb")
46
53
  raise MuError, "Requested to use unsupported grooming agent #{groomer}"
47
54
  end
55
+ begin
48
56
  require "mu/groomers/#{groomer.downcase}"
49
- myclass = Object.const_get("MU").const_get("Groomer").const_get(groomer)
57
+ myclass = Object.const_get("MU").const_get("Groomer").const_get(groomer)
58
+ rescue NameError
59
+ raise MuError, "No groomer available named '#{groomer}' - valid values (case-sensitive) are: #{MU.supportedGroomers.join(", ")})"
60
+ end
50
61
  MU::Groomer.requiredMethods.each { |method|
51
62
  if !myclass.public_instance_methods.include?(method)
52
63
  raise MuError, "MU::Groom::#{groomer} has not implemented required instance method #{method}"
@@ -0,0 +1,553 @@
1
+ # Copyright:: Copyright (c) 2019 eGlobalTech, Inc., all rights reserved
2
+ #
3
+ # Licensed under the BSD-3 license (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License in the root of the project or at
6
+ #
7
+ # http://egt-labs.com/mu/LICENSE.html
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module MU
17
+ # Plugins under this namespace serve as interfaces to host configuration
18
+ # management tools, like Ansible or Puppet.
19
+ class Groomer
20
+ # Support for Ansible as a host configuration management layer.
21
+ class Ansible
22
+
23
+
24
+ # Location in which we'll find our Ansible executables
25
+ BINDIR = "/usr/local/python-current/bin"
26
+ @@pwfile_semaphore = Mutex.new
27
+
28
+ # @param node [MU::Cloud::Server]: The server object on which we'll be operating
29
+ def initialize(node)
30
+ @config = node.config
31
+ @server = node
32
+ @inventory = Inventory.new(node.deploy)
33
+ @mu_user = node.deploy.mu_user
34
+ @ansible_path = node.deploy.deploy_dir+"/ansible"
35
+
36
+ [@ansible_path, @ansible_path+"/roles", @ansible_path+"/vars", @ansible_path+"/group_vars", @ansible_path+"/vaults"].each { |dir|
37
+ if !Dir.exists?(dir)
38
+ MU.log "Creating #{dir}", MU::DEBUG
39
+ Dir.mkdir(dir, 0755)
40
+ end
41
+ }
42
+ MU::Groomer::Ansible.vaultPasswordFile(pwfile: "#{@ansible_path}/.vault_pw")
43
+ installRoles
44
+ end
45
+
46
+
47
+ # Indicate whether our server has been bootstrapped with Ansible
48
+ def haveBootstrapped?
49
+ @inventory.haveNode?(@server.mu_name)
50
+ end
51
+
52
+ # @param vault [String]: A repository of secrets to create/save into.
53
+ # @param item [String]: The item within the repository to create/save.
54
+ # @param data [Hash]: Data to save
55
+ # @param permissions [Boolean]: If true, save the secret under the current active deploy (if any), rather than in the global location for this user
56
+ # @param deploy_dir [String]: If permissions is +true+, save the secret here
57
+ def self.saveSecret(vault: nil, item: nil, data: nil, permissions: false, deploy_dir: nil)
58
+ if vault.nil? or vault.empty? or item.nil? or item.empty?
59
+ raise MuError, "Must call saveSecret with vault and item names"
60
+ end
61
+ if vault.match(/\//) or item.match(/\//) #XXX this should just check for all valid dirname/filename chars
62
+ raise MuError, "Ansible vault/item names cannot include forward slashes"
63
+ end
64
+ pwfile = vaultPasswordFile
65
+
66
+
67
+ dir = if permissions
68
+ if deploy_dir
69
+ deploy_dir+"/ansible/vaults/"+vault
70
+ elsif MU.mommacat
71
+ MU.mommacat.deploy_dir+"/ansible/vaults/"+vault
72
+ else
73
+ raise "MU::Ansible::Groomer.saveSecret had permissions set to true, but I couldn't find an active deploy directory to save into"
74
+ end
75
+ else
76
+ secret_dir+"/"+vault
77
+ end
78
+ path = dir+"/"+item
79
+
80
+ if !Dir.exists?(dir)
81
+ FileUtils.mkdir_p(dir, mode: 0700)
82
+ end
83
+
84
+ if File.exists?(path)
85
+ MU.log "Overwriting existing vault #{vault} item #{item}"
86
+ end
87
+ File.open(path, File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
88
+ f.write data
89
+ }
90
+ cmd = %Q{#{BINDIR}/ansible-vault encrypt #{path} --vault-id #{pwfile}}
91
+ MU.log cmd
92
+ system(cmd)
93
+ end
94
+
95
+ # see {MU::Groomer::Ansible.saveSecret}
96
+ def saveSecret(vault: @server.mu_name, item: nil, data: nil, permissions: true)
97
+ self.class.saveSecret(vault: vault, item: item, data: data, permissions: permissions, deploy_dir: @server.deploy.deploy_dir)
98
+ end
99
+
100
+ # Retrieve sensitive data, which hopefully we're storing and retrieving
101
+ # in a secure fashion.
102
+ # @param vault [String]: A repository of secrets to search
103
+ # @param item [String]: The item within the repository to retrieve
104
+ # @param field [String]: OPTIONAL - A specific field within the item to return.
105
+ # @return [Hash]
106
+ def self.getSecret(vault: nil, item: nil, field: nil)
107
+ if vault.nil? or vault.empty?
108
+ raise MuError, "Must call getSecret with at least a vault name"
109
+ end
110
+
111
+ pwfile = vaultPasswordFile
112
+ dir = secret_dir+"/"+vault
113
+ if !Dir.exists?(dir)
114
+ raise MuNoSuchSecret, "No such vault #{vault}"
115
+ end
116
+
117
+ data = nil
118
+ if item
119
+ itempath = dir+"/"+item
120
+ if !File.exists?(itempath)
121
+ raise MuNoSuchSecret, "No such item #{item} in vault #{vault}"
122
+ end
123
+ cmd = %Q{#{BINDIR}/ansible-vault view #{itempath} --vault-id #{pwfile}}
124
+ MU.log cmd
125
+ a = `#{cmd}`
126
+ # If we happen to have stored recognizeable JSON, return it as parsed,
127
+ # which is a behavior we're used to from Chef vault. Otherwise, return
128
+ # a String.
129
+ begin
130
+ data = JSON.parse(a)
131
+ if field and data[field]
132
+ data = data[field]
133
+ end
134
+ rescue JSON::ParserError
135
+ data = a
136
+ end
137
+ else
138
+ data = []
139
+ Dir.foreach(dir) { |entry|
140
+ next if entry == "." or entry == ".."
141
+ next if File.directory?(dir+"/"+entry)
142
+ data << entry
143
+ }
144
+ end
145
+
146
+ data
147
+ end
148
+
149
+ # see {MU::Groomer::Ansible.getSecret}
150
+ def getSecret(vault: nil, item: nil, field: nil)
151
+ self.class.getSecret(vault: vault, item: item, field: field)
152
+ end
153
+
154
+ # Delete a Ansible data bag / Vault
155
+ # @param vault [String]: A repository of secrets to delete
156
+ def self.deleteSecret(vault: nil, item: nil)
157
+ if vault.nil? or vault.empty?
158
+ raise MuError, "Must call deleteSecret with at least a vault name"
159
+ end
160
+ dir = secret_dir+"/"+vault
161
+ if !Dir.exists?(dir)
162
+ raise MuNoSuchSecret, "No such vault #{vault}"
163
+ end
164
+
165
+ data = nil
166
+ if item
167
+ itempath = dir+"/"+item
168
+ if !File.exists?(itempath)
169
+ raise MuNoSuchSecret, "No such item #{item} in vault #{vault}"
170
+ end
171
+ MU.log "Deleting Ansible vault #{vault} item #{item}", MU::NOTICE
172
+ File.unlink(itempath)
173
+ else
174
+ MU.log "Deleting Ansible vault #{vault}", MU::NOTICE
175
+ FileUtils.rm_rf(dir)
176
+ end
177
+
178
+ end
179
+
180
+ # see {MU::Groomer::Ansible.deleteSecret}
181
+ def deleteSecret(vault: nil, item: nil)
182
+ self.class.deleteSecret(vault: vault, item: nil)
183
+ end
184
+
185
+ # Invoke the Ansible client on the node at the other end of a provided SSH
186
+ # session.
187
+ # @param purpose [String]: A string describing the purpose of this client run.
188
+ # @param max_retries [Integer]: The maximum number of attempts at a successful run to make before giving up.
189
+ # @param output [Boolean]: Display Ansible's regular (non-error) output to the console
190
+ # @param override_runlist [String]: Use the specified run list instead of the node's configured list
191
+ def run(purpose: "Ansible run", update_runlist: true, max_retries: 5, output: true, override_runlist: nil, reboot_first_fail: false, timeout: 1800)
192
+ pwfile = MU::Groomer::Ansible.vaultPasswordFile
193
+ stashHostSSLCertSecret
194
+
195
+ cmd = %Q{cd #{@ansible_path} && #{BINDIR}/ansible-playbook -i hosts #{@server.config['name']}.yml --limit=#{@server.mu_name} --vault-id #{pwfile} --vault-id #{@ansible_path}/.vault_pw}
196
+
197
+ MU.log cmd
198
+ system(cmd)
199
+ end
200
+
201
+ # This is a stub; since Ansible is effectively agentless, this operation
202
+ # doesn't have meaning.
203
+ def preClean(leave_ours = false)
204
+ end
205
+
206
+ # This is a stub; since Ansible is effectively agentless, this operation
207
+ # doesn't have meaning.
208
+ def reinstall
209
+ end
210
+
211
+ # Bootstrap our server with Ansible- basically, just make sure this node
212
+ # is listed in our deployment's Ansible inventory.
213
+ def bootstrap
214
+ @inventory.add(@server.config['name'], @server.mu_name)
215
+ play = {
216
+ "hosts" => @server.config['name']
217
+ }
218
+
219
+ if @server.config['ssh_user'] != "root"
220
+ play["become"] = "yes"
221
+ end
222
+
223
+ if @server.config['run_list'] and !@server.config['run_list'].empty?
224
+ play["roles"] = @server.config['run_list']
225
+ end
226
+
227
+ File.open(@ansible_path+"/"+@server.config['name']+".yml", File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
228
+ f.flock(File::LOCK_EX)
229
+ f.puts [play].to_yaml
230
+ f.flock(File::LOCK_UN)
231
+ }
232
+ end
233
+
234
+ # Synchronize the deployment structure managed by {MU::MommaCat} into some Ansible variables, so that nodes can access this metadata.
235
+ # @return [Hash]: The data synchronized.
236
+ def saveDeployData
237
+ @server.describe(update_cache: true) # Make sure we're fresh
238
+
239
+ allvars = {
240
+ "deployment" => @server.deploy.deployment,
241
+ "service_name" => @config["name"],
242
+ "windows_admin_username" => @config['windows_admin_username'],
243
+ "mu_environment" => MU.environment.downcase,
244
+ }
245
+ allvars['deployment']['ssh_public_key'] = @server.deploy.ssh_public_key
246
+
247
+ if @server.config['cloud'] == "AWS"
248
+ allvars["ec2"] = MU.structToHash(@server.cloud_desc, stringify_keys: true)
249
+ end
250
+
251
+ if @server.windows?
252
+ allvars['windows_admin_username'] = @config['windows_admin_username']
253
+ end
254
+
255
+ if !@server.cloud.nil?
256
+ allvars["cloudprovider"] = @server.cloud
257
+ end
258
+
259
+ File.open(@ansible_path+"/vars/main.yml", File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
260
+ f.flock(File::LOCK_EX)
261
+ f.puts allvars.to_yaml
262
+ f.flock(File::LOCK_UN)
263
+ }
264
+
265
+ groupvars = {}
266
+ if @server.deploy.original_config.has_key?('parameters')
267
+ groupvars["mu_parameters"] = @server.deploy.original_config['parameters']
268
+ end
269
+ if !@config['application_attributes'].nil?
270
+ groupvars["application_attributes"] = @config['application_attributes']
271
+ end
272
+
273
+ File.open(@ansible_path+"/group_vars/"+@server.config['name']+".yml", File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
274
+ f.flock(File::LOCK_EX)
275
+ f.puts groupvars.to_yaml
276
+ f.flock(File::LOCK_UN)
277
+ }
278
+
279
+ allvars['deployment']
280
+ end
281
+
282
+ # Expunge Ansible resources associated with a node.
283
+ # @param node [String]: The Mu name of the node in question.
284
+ # @param vaults_to_clean [Array<Hash>]: Some vaults to expunge
285
+ # @param noop [Boolean]: Skip actual deletion, just state what we'd do
286
+ # @param nodeonly [Boolean]: Just delete the node and its keys, but leave other artifacts
287
+ def self.cleanup(node, vaults_to_clean = [], noop = false, nodeonly: false)
288
+ deploy = MU::MommaCat.new(MU.deploy_id)
289
+ inventory = Inventory.new(deploy)
290
+ ansible_path = deploy.deploy_dir+"/ansible"
291
+ inventory.remove(node)
292
+ end
293
+
294
+ # List the Ansible vaults, if any, owned by the specified Mu user
295
+ # @param user [String]: The user whose vaults we will list
296
+ # @return [Array<String>]
297
+ def self.listSecrets(user = MU.mu_user)
298
+ path = secret_dir(user)
299
+ found = []
300
+ Dir.foreach(path) { |entry|
301
+ next if entry == "." or entry == ".."
302
+ next if !File.directory?(path+"/"+entry)
303
+ found << entry
304
+ }
305
+ found
306
+ end
307
+
308
+ # Encrypt a string using +ansible-vault encrypt_string+ and print the
309
+ # the results to +STDOUT+.
310
+ # @param name [String]: The variable name to use for the string's YAML key
311
+ # @param string [String]: The string to encrypt
312
+ # @param for_user [String]: Encrypt using the Vault password of the specified Mu user
313
+ def self.encryptString(name, string, for_user = nil)
314
+ pwfile = vaultPasswordFile
315
+ cmd = %Q{#{BINDIR}/ansible-vault}
316
+ system(cmd, "encrypt_string", string, "--name", name, "--vault-id", pwfile)
317
+ end
318
+
319
+ private
320
+
321
+ # Get the +.vault_pw+ file for the appropriate user. If it doesn't exist,
322
+ # generate one.
323
+ def self.vaultPasswordFile(for_user = nil, pwfile: nil)
324
+ pwfile ||= secret_dir(for_user)+"/.vault_pw"
325
+ @@pwfile_semaphore.synchronize {
326
+ if !File.exists?(pwfile)
327
+ MU.log "Generating Ansible vault password file at #{pwfile}", MU::DEBUG
328
+ File.open(pwfile, File::CREAT|File::RDWR|File::TRUNC, 0400) { |f|
329
+ f.write Password.random(12..14)
330
+ }
331
+ end
332
+ }
333
+ pwfile
334
+ end
335
+
336
+ # Figure out where our main stash of secrets is, and make sure it exists
337
+ def secret_dir
338
+ MU::Groomer::Ansible.secret_dir(@mu_user)
339
+ end
340
+
341
+ # Figure out where our main stash of secrets is, and make sure it exists
342
+ def self.secret_dir(user = MU.mu_user)
343
+ path = MU.dataDir(user) + "/ansible-secrets"
344
+ Dir.mkdir(path, 0755) if !Dir.exists?(path)
345
+
346
+ path
347
+ end
348
+
349
+ # Make an effort to distinguish an Ansible role from other sorts of
350
+ # artifacts, since 'roles' is an awfully generic name for a directory.
351
+ # Short of a full, slow syntax check, this is the best we're liable to do.
352
+ def isAnsibleRole?(path)
353
+ Dir.foreach(path) { |entry|
354
+ if File.directory?(path+"/"+entry) and
355
+ ["tasks", "vars"].include?(entry)
356
+ return true # https://knowyourmeme.com/memes/close-enough
357
+ elsif ["metadata.rb", "recipes"].include?(entry)
358
+ return false
359
+ end
360
+ }
361
+ false
362
+ end
363
+
364
+ # Find all of the Ansible roles in the various configured Mu repositories
365
+ # and
366
+ def installRoles
367
+ roledir = @ansible_path+"/roles"
368
+
369
+ canon_links = {}
370
+
371
+ # Hook up any Ansible roles listed in our platform repos
372
+ $MU_CFG['repos'].each { |repo|
373
+ repo.match(/\/([^\/]+?)(\.git)?$/)
374
+ shortname = Regexp.last_match(1)
375
+ repodir = MU.dataDir + "/" + shortname
376
+ ["roles", "ansible/roles"].each { |subdir|
377
+ next if !Dir.exists?(repodir+"/"+subdir)
378
+ Dir.foreach(repodir+"/"+subdir) { |role|
379
+ next if [".", ".."].include?(role)
380
+ realpath = repodir+"/"+subdir+"/"+role
381
+ link = roledir+"/"+role
382
+
383
+ if isAnsibleRole?(realpath)
384
+ if !File.exists?(link)
385
+ File.symlink(realpath, link)
386
+ canon_links[role] = realpath
387
+ elsif File.symlink?(link)
388
+ cur_target = File.readlink(link)
389
+ if cur_target == realpath
390
+ canon_links[role] = realpath
391
+ elsif !canon_links[role]
392
+ File.unlink(link)
393
+ File.symlink(realpath, link)
394
+ canon_links[role] = realpath
395
+ end
396
+ end
397
+ end
398
+ }
399
+ }
400
+ }
401
+
402
+ # Now layer on everything bundled in the main Mu repo
403
+ Dir.foreach(MU.myRoot+"/ansible/roles") { |role|
404
+ next if [".", ".."].include?(role)
405
+ next if File.exists?(roledir+"/"+role)
406
+ File.symlink(MU.myRoot+"/ansible/roles/"+role, roledir+"/"+role)
407
+ }
408
+
409
+ if @server.config['run_list']
410
+ @server.config['run_list'].each { |role|
411
+ found = false
412
+ if !File.exists?(roledir+"/"+role)
413
+ if role.match(/[^\.]\.[^\.]/) and @server.config['groomer_autofetch']
414
+ system(%Q{#{BINDIR}/ansible-galaxy}, "--roles-path", roledir, "install", role)
415
+ found = true
416
+ # XXX check return value
417
+ else
418
+ canon_links.keys.each { |longrole|
419
+ if longrole.match(/\.#{Regexp.quote(role)}$/)
420
+ File.symlink(roledir+"/"+longrole, roledir+"/"+role)
421
+ found = true
422
+ break
423
+ end
424
+ }
425
+ end
426
+ else
427
+ found = true
428
+ end
429
+ if !found
430
+ raise MuError, "Unable to locate Ansible role #{role}"
431
+ end
432
+ }
433
+ end
434
+ end
435
+
436
+ # Upload the certificate to a Chef Vault for this node
437
+ def stashHostSSLCertSecret
438
+ cert, key = @server.deploy.nodeSSLCerts(@server)
439
+ certdata = {
440
+ "data" => {
441
+ "node.crt" => cert.to_pem.chomp!.gsub(/\n/, "\\n"),
442
+ "node.key" => key.to_pem.chomp!.gsub(/\n/, "\\n")
443
+ }
444
+ }
445
+ saveSecret(item: "ssl_cert", data: certdata, permissions: true)
446
+
447
+ saveSecret(item: "secrets", data: @config['secrets'], permissions: true) if !@config['secrets'].nil?
448
+ certdata
449
+ end
450
+
451
+ # Simple interface for an Ansible inventory file.
452
+ class Inventory
453
+
454
+ # @param deploy [MU::MommaCat]
455
+ def initialize(deploy)
456
+ @deploy = deploy
457
+ @ansible_path = @deploy.deploy_dir+"/ansible"
458
+ if !Dir.exists?(@ansible_path)
459
+ Dir.mkdir(@ansible_path, 0755)
460
+ end
461
+
462
+ @lockfile = File.open(@ansible_path+"/.hosts.lock", File::CREAT|File::RDWR, 0600)
463
+ end
464
+
465
+ # See if we have a particular node in our inventory.
466
+ def haveNode?(name)
467
+ lock
468
+ read
469
+ @inv.each_pair { |group, nodes|
470
+ if nodes.include?(name)
471
+ unlock
472
+ return true
473
+ end
474
+ }
475
+ unlock
476
+ false
477
+ end
478
+
479
+ # Add a node to our Ansible inventory
480
+ # @param group [String]: The host group to which the node belongs
481
+ # @param name [String]: The hostname or IP of the node
482
+ def add(group, name)
483
+ if group.nil? or group.empty? or name.nil? or name.empty?
484
+ raise MuError, "Ansible::Inventory.add requires both a host group string and a name"
485
+ end
486
+ lock
487
+ read
488
+ @inv[group] ||= []
489
+ @inv[group] << name
490
+ @inv[group].uniq!
491
+ save!
492
+ unlock
493
+ end
494
+
495
+ # Remove a node from our Ansible inventory
496
+ # @param name [String]: The hostname or IP of the node
497
+ def remove(name)
498
+ lock
499
+ read
500
+ @inv.each_pair { |group, nodes|
501
+ nodes.delete(name)
502
+ }
503
+ save!
504
+ unlock
505
+ end
506
+
507
+ private
508
+
509
+ def lock
510
+ @lockfile.flock(File::LOCK_EX)
511
+ end
512
+
513
+ def unlock
514
+ @lockfile.flock(File::LOCK_UN)
515
+ end
516
+
517
+ def save!
518
+ @inv ||= {}
519
+
520
+ File.open(@ansible_path+"/hosts", File::CREAT|File::RDWR|File::TRUNC, 0600) { |f|
521
+ @inv.each_pair { |group, hosts|
522
+ next if hosts.size == 0 # don't write empty groups
523
+ f.puts "["+group+"]"
524
+ f.puts hosts.join("\n")
525
+ }
526
+ }
527
+ end
528
+
529
+ def read
530
+ @inv = {}
531
+ if File.exists?(@ansible_path+"/hosts")
532
+ section = nil
533
+ File.readlines(@ansible_path+"/hosts").each { |l|
534
+ l.chomp!
535
+ l.sub!(/#.*/, "")
536
+ next if l.empty?
537
+ if l.match(/\[(.+?)\]/)
538
+ section = Regexp.last_match[1]
539
+ @inv[section] ||= []
540
+ else
541
+ @inv[section] << l
542
+ end
543
+ }
544
+ end
545
+
546
+ @inv
547
+ end
548
+
549
+ end
550
+
551
+ end # class Ansible
552
+ end # class Groomer
553
+ end # Module Mu