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.
- checksums.yaml +5 -5
- data/README.md +6 -0
- data/ansible/roles/geerlingguy.firewall/LICENSE +20 -0
- data/ansible/roles/geerlingguy.firewall/README.md +93 -0
- data/ansible/roles/geerlingguy.firewall/defaults/main.yml +19 -0
- data/ansible/roles/geerlingguy.firewall/handlers/main.yml +3 -0
- data/ansible/roles/geerlingguy.firewall/meta/main.yml +26 -0
- data/ansible/roles/geerlingguy.firewall/molecule/default/molecule.yml +40 -0
- data/ansible/roles/geerlingguy.firewall/molecule/default/playbook.yml +17 -0
- data/ansible/roles/geerlingguy.firewall/molecule/default/tests/test_default.py +14 -0
- data/ansible/roles/geerlingguy.firewall/molecule/default/yaml-lint.yml +6 -0
- data/ansible/roles/geerlingguy.firewall/tasks/disable-other-firewalls.yml +66 -0
- data/ansible/roles/geerlingguy.firewall/tasks/main.yml +44 -0
- data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +136 -0
- data/ansible/roles/geerlingguy.firewall/templates/firewall.init.j2 +52 -0
- data/ansible/roles/geerlingguy.firewall/templates/firewall.unit.j2 +12 -0
- data/bin/mu-ansible-secret +114 -0
- data/bin/mu-aws-setup +74 -21
- data/bin/mu-node-manage +22 -12
- data/bin/mu-self-update +11 -4
- data/cloud-mu.gemspec +3 -3
- data/cookbooks/firewall/metadata.json +1 -1
- data/cookbooks/firewall/recipes/default.rb +4 -0
- data/cookbooks/mu-master/recipes/default.rb +0 -3
- data/cookbooks/mu-master/recipes/init.rb +15 -9
- data/cookbooks/mu-master/templates/default/mu.rc.erb +1 -1
- data/cookbooks/mu-master/templates/default/web_app.conf.erb +0 -4
- data/cookbooks/mu-php54/metadata.rb +2 -2
- data/cookbooks/mu-php54/recipes/default.rb +1 -3
- data/cookbooks/mu-tools/recipes/eks.rb +25 -2
- data/cookbooks/mu-tools/recipes/nrpe.rb +6 -1
- data/cookbooks/mu-tools/recipes/set_mu_hostname.rb +8 -0
- data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
- data/cookbooks/mu-tools/templates/default/kubeconfig.erb +2 -2
- data/cookbooks/mu-tools/templates/default/kubelet-config.json.erb +35 -0
- data/extras/clean-stock-amis +10 -4
- data/extras/list-stock-amis +64 -0
- data/extras/python_rpm/build.sh +21 -0
- data/extras/python_rpm/muthon.spec +68 -0
- data/install/README.md +5 -2
- data/install/user-dot-murc.erb +1 -1
- data/modules/mu.rb +52 -8
- data/modules/mu/clouds/aws.rb +1 -1
- data/modules/mu/clouds/aws/container_cluster.rb +1071 -47
- data/modules/mu/clouds/aws/firewall_rule.rb +45 -19
- data/modules/mu/clouds/aws/log.rb +3 -2
- data/modules/mu/clouds/aws/role.rb +18 -2
- data/modules/mu/clouds/aws/server.rb +11 -5
- data/modules/mu/clouds/aws/server_pool.rb +20 -24
- data/modules/mu/clouds/aws/userdata/linux.erb +1 -1
- data/modules/mu/clouds/aws/vpc.rb +9 -0
- data/modules/mu/clouds/google/server.rb +2 -0
- data/modules/mu/config.rb +3 -3
- data/modules/mu/config/container_cluster.rb +1 -1
- data/modules/mu/config/firewall_rule.rb +4 -0
- data/modules/mu/config/role.rb +29 -0
- data/modules/mu/config/server.rb +9 -4
- data/modules/mu/groomer.rb +14 -3
- data/modules/mu/groomers/ansible.rb +553 -0
- data/modules/mu/groomers/chef.rb +0 -5
- data/modules/mu/mommacat.rb +18 -3
- data/modules/scratchpad.erb +1 -1
- data/requirements.txt +5 -0
- metadata +39 -16
data/modules/mu/config/server.rb
CHANGED
@@ -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
|
-
|
136
|
-
|
137
|
-
|
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" => "
|
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" => {
|
data/modules/mu/groomer.rb
CHANGED
@@ -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
|
-
|
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
|