knife-tidy 2.0.1 → 2.0.15
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/conf/substitutions.json.example +10 -0
- data/lib/chef/knife/tidy_backup_clean.rb +23 -26
- data/lib/chef/knife/tidy_base.rb +2 -2
- data/lib/chef/knife/tidy_notify.rb +31 -31
- data/lib/chef/knife/tidy_server_clean.rb +5 -3
- data/lib/chef/knife/tidy_server_report.rb +24 -4
- data/lib/chef/tidy_acls.rb +13 -12
- data/lib/chef/tidy_common.rb +122 -1
- data/lib/chef/tidy_server.rb +1 -1
- data/lib/chef/tidy_substitutions.rb +3 -10
- data/lib/knife-tidy/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7aaf34102e27291f03892bfb4eddb450aaed46c5e615818e48a59c174c91e42
|
4
|
+
data.tar.gz: f482bbfb1182b581532ef643900c6f4814dc800ff42e02ff0a29b8936faed77b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5192215cbd8341aef601e71bf6c42bb2eb5d19f652570734ac192151941d7b954da8d0fe5c504ea67b599772d09ea12da77d6d782f54e2c967c6c12fc9cef0d
|
7
|
+
data.tar.gz: 75b37b5663f0558791ad1a52b5281d84197e1def8ed2c15103896165a781b4a07f1ac4556f5d266e35fb662d76218e6be0ccf413d569e0b7b776075e5a5aca27
|
@@ -1,15 +1,15 @@
|
|
1
|
-
|
1
|
+
require_relative "tidy_base"
|
2
2
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
5
5
|
class TidyBackupClean < Knife
|
6
6
|
deps do
|
7
|
-
require "chef/
|
7
|
+
require "chef/cookbook/cookbook_version_loader"
|
8
8
|
require "chef/cookbook/metadata"
|
9
9
|
require "chef/role"
|
10
10
|
require "chef/run_list"
|
11
|
-
|
12
|
-
|
11
|
+
require_relative "../tidy_substitutions"
|
12
|
+
require_relative "../tidy_acls"
|
13
13
|
require "ffi_yajl"
|
14
14
|
require "fileutils"
|
15
15
|
require "securerandom"
|
@@ -75,7 +75,7 @@ class Chef
|
|
75
75
|
tidy.global_user_names.each do |user|
|
76
76
|
email = ""
|
77
77
|
ui.stdout.puts "INFO: Validating #{user}"
|
78
|
-
the_user =
|
78
|
+
the_user = tidy.json_file_to_hash(File.join(tidy.users_path, "#{user}.json"), symbolize_names: false)
|
79
79
|
if the_user.key?("email") && the_user["email"].match(/\A[^@\s]+@[^@\s]+\z/)
|
80
80
|
if emails_seen.include?(the_user["email"])
|
81
81
|
ui.stdout.puts "REPAIRING: Already saw #{user}'s email, creating a unique one."
|
@@ -147,7 +147,7 @@ class Chef
|
|
147
147
|
add_cookbook_name_to_metadata(cookbook_name, rb_path) if lines.empty?
|
148
148
|
else
|
149
149
|
if ::File.exist?(json_path)
|
150
|
-
metadata =
|
150
|
+
metadata = tidy.json_file_to_hash(json_path, symbolize_names: false)
|
151
151
|
if metadata["name"] != cookbook_name
|
152
152
|
metadata["name"] = cookbook_name
|
153
153
|
ui.stdout.puts "REPAIRING: Correcting `name` in #{json_path}`"
|
@@ -161,26 +161,24 @@ class Chef
|
|
161
161
|
end
|
162
162
|
|
163
163
|
def load_cookbooks(org)
|
164
|
-
|
165
|
-
|
164
|
+
for_each_cookbook_path(org) do |cookbook|
|
165
|
+
cl = Chef::Cookbook::CookbookVersionLoader.new(cookbook)
|
166
166
|
ui.stdout.puts "INFO: Loading #{cookbook}"
|
167
|
-
ret = cl.
|
167
|
+
ret = cl.load!
|
168
168
|
if ret.nil?
|
169
169
|
action_needed("ACTION NEEDED: Something's wrong with the #{cookbook} cookbook in org #{org} - cannot load it! Moving to cookbooks.broken folder.")
|
170
170
|
broken_cookooks_add(org, cookbook)
|
171
171
|
end
|
172
172
|
end
|
173
|
-
rescue LoadError => e
|
173
|
+
rescue LoadError, Exceptions::MetadataNotValid => e
|
174
174
|
ui.error e
|
175
175
|
exit 1
|
176
176
|
end
|
177
177
|
|
178
|
-
def broken_cookooks_add(org,
|
178
|
+
def broken_cookooks_add(org, cookbook_path)
|
179
179
|
broken_path = ::File.join(tidy.org_path(org), "cookbooks.broken")
|
180
180
|
FileUtils.mkdir(broken_path) unless ::File.directory?(broken_path)
|
181
|
-
|
182
|
-
FileUtils.mv(cb, broken_path, verbose: true, force: true)
|
183
|
-
end
|
181
|
+
FileUtils.mv(cookbook_path, broken_path, verbose: true, force: true)
|
184
182
|
end
|
185
183
|
|
186
184
|
def generate_new_metadata(org)
|
@@ -194,7 +192,7 @@ class Chef
|
|
194
192
|
Dir[::File.join(tidy.backup_path, "organizations/*/cookbooks/chef-sugar*/metadata.rb")].each do |file|
|
195
193
|
ui.stdout.puts "INFO: Searching for known chef-sugar problems when uploading."
|
196
194
|
s = Chef::TidySubstitutions.new(nil, tidy)
|
197
|
-
version =
|
195
|
+
version = tidy.cookbook_version_from_path(::File.dirname(file))
|
198
196
|
patterns = [
|
199
197
|
{
|
200
198
|
search: "^require .*/lib/chef/sugar/version",
|
@@ -222,13 +220,14 @@ class Chef
|
|
222
220
|
Chef::TidySubstitutions.new(nil, tidy).sub_in_file(
|
223
221
|
::File.join(cookbook_path, "metadata.rb"),
|
224
222
|
Regexp.new("^depends +['\"]#{name}['\"]"),
|
225
|
-
"# depends '#{name}' # knife-tidy was here"
|
223
|
+
"# depends '#{name}' # knife-tidy was here"
|
224
|
+
)
|
226
225
|
end
|
227
226
|
end
|
228
227
|
|
229
228
|
def fix_metadata_fields(cookbook_path)
|
230
229
|
json_path = ::File.join(cookbook_path, "metadata.json")
|
231
|
-
metadata =
|
230
|
+
metadata = tidy.json_file_to_hash(json_path, symbolize_names: false)
|
232
231
|
md = metadata.dup
|
233
232
|
metadata.each_pair do |key, value|
|
234
233
|
if value.nil?
|
@@ -279,9 +278,7 @@ class Chef
|
|
279
278
|
|
280
279
|
def create_minimal_metadata(cookbook_path)
|
281
280
|
name = tidy.cookbook_name_from_path(cookbook_path)
|
282
|
-
|
283
|
-
name_version = components[components.index("cookbooks") + 1]
|
284
|
-
version = name_version.match(/\d+\.\d+\.\d+/).to_s
|
281
|
+
version = tidy.cookbook_version_from_path(cookbook_path)
|
285
282
|
metadata = {}
|
286
283
|
metadata["name"] = name
|
287
284
|
metadata["version"] = version
|
@@ -352,7 +349,7 @@ class Chef
|
|
352
349
|
end
|
353
350
|
|
354
351
|
def repair_role_run_lists(role_path)
|
355
|
-
the_role =
|
352
|
+
the_role = tidy.json_file_to_hash(role_path, symbolize_names: false)
|
356
353
|
new_role = the_role.clone
|
357
354
|
rl = Chef::RunList.new
|
358
355
|
new_role["run_list"] = []
|
@@ -378,14 +375,14 @@ class Chef
|
|
378
375
|
end
|
379
376
|
end
|
380
377
|
write_role(role_path, new_role)
|
381
|
-
# rubocop:enable MethodLength
|
378
|
+
# rubocop:enable Metrics/MethodLength
|
382
379
|
end
|
383
380
|
|
384
381
|
def validate_roles(org)
|
385
382
|
for_each_role(org) do |role_path|
|
386
383
|
ui.stdout.puts "INFO: Validating Role at #{role_path}"
|
387
384
|
begin
|
388
|
-
Chef::Role.from_hash(
|
385
|
+
Chef::Role.from_hash(tidy.json_file_to_hash(role_path, symbolize_names: false))
|
389
386
|
rescue ArgumentError
|
390
387
|
repair_role_run_lists(role_path)
|
391
388
|
end
|
@@ -395,10 +392,10 @@ class Chef
|
|
395
392
|
def validate_clients_group(org)
|
396
393
|
ui.stdout.puts "INFO: validating all clients for org #{org} exist in clients group"
|
397
394
|
clients_group_path = ::File.join(tidy.groups_path(org), "clients.json")
|
398
|
-
existing_group_data =
|
395
|
+
existing_group_data = tidy.json_file_to_hash(clients_group_path, symbolize_names: false)
|
399
396
|
existing_group_data["clients"] = [] unless existing_group_data.key?("clients")
|
400
397
|
if existing_group_data["clients"].length != tidy.client_names(org).length
|
401
|
-
ui.stdout.puts "REPAIRING: Adding #{(existing_group_data[
|
398
|
+
ui.stdout.puts "REPAIRING: Adding #{(existing_group_data["clients"].length - tidy.client_names(org).length).abs} missing clients into #{org}'s client group file #{clients_group_path}"
|
402
399
|
existing_group_data["clients"] = (existing_group_data["clients"] + tidy.client_names(org)).uniq
|
403
400
|
::File.open(clients_group_path, "w") do |f|
|
404
401
|
f.write(Chef::JSONCompat.to_json_pretty(existing_group_data))
|
@@ -409,7 +406,7 @@ class Chef
|
|
409
406
|
def validate_invitations(org)
|
410
407
|
invite_file = tidy.invitations_path(org)
|
411
408
|
ui.stdout.puts "INFO: validating org #{org} invites in #{invite_file}"
|
412
|
-
invitations =
|
409
|
+
invitations = tidy.json_file_to_hash(invite_file, symbolize_names: false)
|
413
410
|
invitations_new = []
|
414
411
|
invitations.each do |invite|
|
415
412
|
if invite["username"].nil?
|
data/lib/chef/knife/tidy_base.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative "tidy_base"
|
2
2
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
@@ -11,42 +11,42 @@ class Chef
|
|
11
11
|
banner "knife tidy notify (options)"
|
12
12
|
|
13
13
|
option :smtp_server,
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
short: "-s SERVER_NAME",
|
15
|
+
long: "--smtp_server SERVER_NAME",
|
16
|
+
default: "localhost",
|
17
|
+
description: "SMTP Server to be used for emailling reports to organization admins (defaults to localhost)"
|
18
18
|
|
19
19
|
option :smtp_port,
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
short: "-p SMTP_PORT",
|
21
|
+
long: "--smtp_port SMTP_PORT",
|
22
|
+
default: 25,
|
23
|
+
description: "SMTP port to be used for emailling reports to organization admins (defaults to 25)"
|
24
24
|
|
25
25
|
option :smtp_helo,
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
short: "-h SMTP_HELO",
|
27
|
+
long: "--smtp_helo SMTP_HELO",
|
28
|
+
default: "localhost",
|
29
|
+
description: "SMTP HELO to be used for emailling reports to organization admins (defaults to localhost)"
|
30
30
|
|
31
31
|
option :smtp_username,
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
short: "-u SMTP_USERNAME",
|
33
|
+
long: "--smtp_username SMTP_USERNAME",
|
34
|
+
description: "SMTP Username to be used for emailling reports to organization admins"
|
35
35
|
|
36
36
|
option :smtp_password,
|
37
|
-
|
38
|
-
|
37
|
+
long: "--smtp_password SMTP_PASSWORD",
|
38
|
+
description: "SMTP Password to be used for emailling reports to organization admins"
|
39
39
|
|
40
40
|
option :smtp_from,
|
41
|
-
|
42
|
-
|
41
|
+
long: "--smtp_from SMTP_FROM",
|
42
|
+
description: "SMTP From address to be used for emailling reports to organization admins"
|
43
43
|
|
44
44
|
option :smtp_use_tls,
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
long: "--smtp_use_tls",
|
46
|
+
short: "-t",
|
47
|
+
default: false,
|
48
|
+
boolean: true | false,
|
49
|
+
description: "Whether TLS should be used for emailling reports to organization admins (defaults to false if omitted)"
|
50
50
|
|
51
51
|
include Knife::TidyBase
|
52
52
|
|
@@ -54,13 +54,13 @@ class Chef
|
|
54
54
|
reports_dir = tidy.reports_dir
|
55
55
|
report_file_suffixes = ["_unused_cookbooks.json", "_cookbook_count.json", "_stale_nodes.json"]
|
56
56
|
# Only grab the files matching the report_file_suffixes
|
57
|
-
report_files = Dir["#{reports_dir}/*{#{report_file_suffixes.join(
|
57
|
+
report_files = Dir["#{reports_dir}/*{#{report_file_suffixes.join(",")}}"]
|
58
58
|
|
59
59
|
ui.info "Reading from #{tidy.reports_dir} directory"
|
60
60
|
|
61
61
|
# Fetch list of organization names from reports directory
|
62
62
|
begin
|
63
|
-
org_names = report_files.map { |r_file| r_file.match("#{reports_dir}\/(.*)(#{report_file_suffixes.join(
|
63
|
+
org_names = report_files.map { |r_file| r_file.match("#{reports_dir}\/(.*)(#{report_file_suffixes.join("|")})").captures.first }.uniq
|
64
64
|
rescue NoMethodError
|
65
65
|
ui.stderr.puts "Failed to parse json reports files. Please ensure your reports are valid."
|
66
66
|
return
|
@@ -88,7 +88,7 @@ class Chef
|
|
88
88
|
file_name = "#{reports_dir}/#{org}#{report}"
|
89
89
|
ui.info(" Parsing file #{file_name}")
|
90
90
|
json_string = File.read(file_name)
|
91
|
-
reports[org][report] =
|
91
|
+
reports[org][report] = tidy.json_file_to_hash(json_string, symbolize_names: false)
|
92
92
|
rescue Errno::ENOENT
|
93
93
|
ui.info(" Skipping file #{file_name} - not found for organization #{org}")
|
94
94
|
reports[org][report] = {}
|
@@ -120,7 +120,7 @@ class Chef
|
|
120
120
|
mime_boundary = "==Multipart_Boundary_x#{srand}x"
|
121
121
|
message = <<~MESSAGE_END
|
122
122
|
From: Knife Tidy <#{config[:smtp_from]}>
|
123
|
-
To: #{recipients.map { |recipient| "#{recipient[:name]} <#{recipient[:email]}>" }.join(
|
123
|
+
To: #{recipients.map { |recipient| "#{recipient[:name]} <#{recipient[:email]}>" }.join(", ")}
|
124
124
|
MIME-Version: 1.0
|
125
125
|
Subject: Knife Tidy Cleanup Report for Organization "#{organization}"
|
126
126
|
Content-Type: multipart/mixed; boundary="#{mime_boundary}";
|
@@ -172,13 +172,13 @@ class Chef
|
|
172
172
|
table_body = if report_data[organization]["_unused_cookbooks.json"].empty?
|
173
173
|
"<tr><td colspan='2'>No unused cookbook versions</td></tr>"
|
174
174
|
else
|
175
|
-
report_data[organization]["_unused_cookbooks.json"].map { |cookbook_name, cookbook_versions| "<tr><td>#{cookbook_name}</td><td>#{cookbook_versions.join(
|
175
|
+
report_data[organization]["_unused_cookbooks.json"].map { |cookbook_name, cookbook_versions| "<tr><td>#{cookbook_name}</td><td>#{cookbook_versions.join("<br>")}</td></tr>" }.join("\n")
|
176
176
|
end
|
177
177
|
table_start + header_string + table_body + table_end
|
178
178
|
end
|
179
179
|
|
180
180
|
def generate_node_table(report_data, organization)
|
181
|
-
table_start = "<h2>Stale Nodes</h2><p>This table contains nodes that have not checked in to the Chef Server in #{report_data[organization][
|
181
|
+
table_start = "<h2>Stale Nodes</h2><p>This table contains nodes that have not checked in to the Chef Server in #{report_data[organization]["_stale_nodes.json"]["threshold_days"]} days.<p><table border='1' cellpadding='1' cellspacing='0'>"
|
182
182
|
table_end = "</table>"
|
183
183
|
header_string = "<tr><th>Node Name</th></tr>"
|
184
184
|
table_body = if report_data[organization]["_stale_nodes.json"].empty? || report_data[organization]["_stale_nodes.json"]["count"] == 0
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative "tidy_base"
|
2
2
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
@@ -89,8 +89,9 @@ class Chef
|
|
89
89
|
queue = Chef::Util::ThreadedJobQueue.new
|
90
90
|
unused_cookbooks_file = ::File.join(tidy.reports_dir, "#{org}_unused_cookbooks.json")
|
91
91
|
return unless ::File.exist?(unused_cookbooks_file)
|
92
|
+
|
92
93
|
ui.stdout.puts "INFO: Cleaning cookbooks for Org: #{org}, using #{unused_cookbooks_file}"
|
93
|
-
unused_cookbooks =
|
94
|
+
unused_cookbooks = tidy.json_file_to_hash(unused_cookbooks_file, symbolize_names: true)
|
94
95
|
unused_cookbooks.keys.each do |cookbook|
|
95
96
|
versions = unused_cookbooks[cookbook]
|
96
97
|
versions.each do |version|
|
@@ -115,8 +116,9 @@ class Chef
|
|
115
116
|
queue = Chef::Util::ThreadedJobQueue.new
|
116
117
|
stale_nodes_file = ::File.join(tidy.reports_dir, "#{org}_stale_nodes.json")
|
117
118
|
return unless ::File.exist?(stale_nodes_file)
|
119
|
+
|
118
120
|
ui.stdout.puts "INFO: Cleaning stale nodes for Org: #{org}, using #{stale_nodes_file}"
|
119
|
-
stale_nodes =
|
121
|
+
stale_nodes = tidy.json_file_to_hash(stale_nodes_file, symbolize_names: true)
|
120
122
|
stale_nodes[:list].each do |node|
|
121
123
|
queue << -> { delete_node_job(org, node) }
|
122
124
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative "tidy_base"
|
2
2
|
|
3
3
|
class Chef
|
4
4
|
class Knife
|
@@ -17,6 +17,11 @@ class Chef
|
|
17
17
|
default: 30,
|
18
18
|
description: "Maximum number of days since last checkin before node is considered stale (default: 30)"
|
19
19
|
|
20
|
+
option :keep_versions,
|
21
|
+
long: "--keep-versions MIN",
|
22
|
+
default: 0,
|
23
|
+
description: "Keep a minimum of this many versions of each cookbook (default: 0)"
|
24
|
+
|
20
25
|
def run
|
21
26
|
ensure_reports_dir!
|
22
27
|
FileUtils.rm_f(server_warnings_file_path)
|
@@ -32,6 +37,7 @@ class Chef
|
|
32
37
|
|
33
38
|
stale_orgs = []
|
34
39
|
node_threshold = config[:node_threshold].to_i
|
40
|
+
keep_versions = config[:keep_versions].to_i
|
35
41
|
|
36
42
|
orgs.each do |org|
|
37
43
|
pre_12_3_nodes = []
|
@@ -43,7 +49,7 @@ class Chef
|
|
43
49
|
nodes = nodes_list(org)
|
44
50
|
db_nodes = rest.get("/organizations/#{org}/nodes")
|
45
51
|
unless nodes.length == db_nodes.length
|
46
|
-
ood_message = "Search index is out of date! No
|
52
|
+
ood_message = "Search index is out of date (search returned #{nodes.length} nodes while the database indicates there are #{db_nodes.length} nodes! No action will be taken for #{org}. Perhaps a 'chef-server-ctl reindex' is in order?"
|
47
53
|
ui.error(ood_message)
|
48
54
|
action_needed(ood_message, server_warnings_file_path)
|
49
55
|
next
|
@@ -51,7 +57,7 @@ class Chef
|
|
51
57
|
|
52
58
|
nodes.each do |node|
|
53
59
|
# If the node hasn't checked in.
|
54
|
-
|
60
|
+
unless node["chef_packages"]
|
55
61
|
# If the node is under an hour old.
|
56
62
|
if (Time.now.to_i - node["ohai_time"].to_i) < 3600
|
57
63
|
unconverged_recent_nodes << node["name"]
|
@@ -76,6 +82,9 @@ class Chef
|
|
76
82
|
end
|
77
83
|
end
|
78
84
|
|
85
|
+
used_cookbooks = keep_cookbook_versions(cb_list, keep_versions)
|
86
|
+
|
87
|
+
Chef::Log.debug("Used cookbook list before checking environments: #{used_cookbooks}")
|
79
88
|
pins = environment_constraints(org)
|
80
89
|
used_cookbooks = check_environment_pins(used_cookbooks, pins, cb_list)
|
81
90
|
|
@@ -153,6 +162,15 @@ class Chef
|
|
153
162
|
cb_list
|
154
163
|
end
|
155
164
|
|
165
|
+
def keep_cookbook_versions(cb_list, min)
|
166
|
+
retain = {}
|
167
|
+
cb_list.each do |name, versions|
|
168
|
+
keep = versions.sort { |a, b| Gem::Version.new(a) <=> Gem::Version.new(b) }.last(min)
|
169
|
+
retain[name] = keep
|
170
|
+
end
|
171
|
+
retain
|
172
|
+
end
|
173
|
+
|
156
174
|
def cookbook_count(cb_list)
|
157
175
|
cb_count_list = {}
|
158
176
|
cb_list.each do |name, versions|
|
@@ -207,8 +225,9 @@ class Chef
|
|
207
225
|
else
|
208
226
|
versions_not_satisfied.push(v)
|
209
227
|
end
|
228
|
+
|
210
229
|
if v == cb_list[cb].last
|
211
|
-
ui.warn("Pin of #{cb} #{version} not satisfied by current versions of cookbook: [#{versions_not_satisfied.join(
|
230
|
+
ui.warn("Pin of #{cb} #{version} not satisfied by current versions of cookbook: [#{versions_not_satisfied.join(", ")}]")
|
212
231
|
end
|
213
232
|
end
|
214
233
|
else
|
@@ -221,6 +240,7 @@ class Chef
|
|
221
240
|
pins.each do |cb, versions|
|
222
241
|
versions.each do |version|
|
223
242
|
next if version == "<= 0.0.0"
|
243
|
+
|
224
244
|
if used_cookbooks[cb]
|
225
245
|
# This pinned cookbook is in the used list, now check for a matching version.
|
226
246
|
used_cookbooks[cb].each do |v|
|
data/lib/chef/tidy_acls.rb
CHANGED
@@ -20,26 +20,26 @@ class Chef
|
|
20
20
|
def load_users
|
21
21
|
@tidy.ui.stdout.puts "INFO: Loading users"
|
22
22
|
Dir[::File.join(@tidy.users_path, "*.json")].each do |user|
|
23
|
-
@users.push(
|
23
|
+
@users.push(@tidy.json_file_to_hash(user, symbolize_names: true))
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
def load_members
|
28
28
|
@tidy.ui.stdout.puts "INFO: Loading members for #{@org}"
|
29
|
-
@members =
|
29
|
+
@members = @tidy.json_file_to_hash(@tidy.members_path(@org), symbolize_names: true)
|
30
30
|
end
|
31
31
|
|
32
32
|
def load_clients
|
33
33
|
@tidy.ui.stdout.puts "INFO: Loading clients for #{@org}"
|
34
34
|
Dir[::File.join(@tidy.clients_path(@org), "*.json")].each do |client|
|
35
|
-
@clients.push(
|
35
|
+
@clients.push(@tidy.json_file_to_hash(client, symbolize_names: true))
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
39
|
def load_groups
|
40
40
|
@tidy.ui.stdout.puts "INFO: Loading groups for #{@org}"
|
41
41
|
Dir[::File.join(@tidy.groups_path(@org), "*.json")].each do |group|
|
42
|
-
@groups.push(
|
42
|
+
@groups.push(@tidy.json_file_to_hash(group, symbolize_names: true))
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
@@ -128,7 +128,7 @@ class Chef
|
|
128
128
|
|
129
129
|
def remove_group_from_acl(group, acl_file)
|
130
130
|
@tidy.ui.stdout.puts "REPAIRING: Removing invalid group: #{group} from #{acl_file}"
|
131
|
-
acl =
|
131
|
+
acl = @tidy.json_file_to_hash(acl_file, symbolize_names: false)
|
132
132
|
acl_ops.each do |op|
|
133
133
|
acl[op]["groups"].reject! { |the_group| the_group == group }
|
134
134
|
end
|
@@ -137,7 +137,7 @@ class Chef
|
|
137
137
|
|
138
138
|
# Appends the proper acls for ::server-admins and the org's read access group if they are missing.
|
139
139
|
def ensure_global_group_acls(acl_file)
|
140
|
-
acl =
|
140
|
+
acl = @tidy.json_file_to_hash(acl_file, symbolize_names: false)
|
141
141
|
acl_ops.each do |op|
|
142
142
|
unless acl[op]["groups"].include? "::server-admins"
|
143
143
|
@tidy.ui.stdout.puts "REPAIRING: Adding #{op} acl for ::server-admins in #{acl_file}"
|
@@ -152,7 +152,7 @@ class Chef
|
|
152
152
|
end
|
153
153
|
|
154
154
|
def ensure_client_read_acls(acl_file)
|
155
|
-
acl =
|
155
|
+
acl = @tidy.json_file_to_hash(acl_file, symbolize_names: false)
|
156
156
|
%w{users admins}.each do |group|
|
157
157
|
unless acl["read"]["groups"].include? group
|
158
158
|
@tidy.ui.stdout.puts "REPAIRING: Adding read acl for #{group} in #{acl_file}"
|
@@ -164,10 +164,11 @@ class Chef
|
|
164
164
|
|
165
165
|
def validate_acls
|
166
166
|
org_acls.each do |acl_file|
|
167
|
-
acl =
|
167
|
+
acl = @tidy.json_file_to_hash(acl_file, symbolize_names: false)
|
168
168
|
actors_groups = acl_actors_groups(acl)
|
169
169
|
actors_groups[:actors].each do |actor|
|
170
170
|
next if actor == "pivotal"
|
171
|
+
|
171
172
|
if ambiguous_actor?(actor)
|
172
173
|
fix_ambiguous_actor(actor)
|
173
174
|
elsif missing_from_members?(actor)
|
@@ -202,11 +203,11 @@ class Chef
|
|
202
203
|
@members.each do |member|
|
203
204
|
user_acl_path = ::File.join(@tidy.user_acls_path, "#{member[:user][:username]}.json")
|
204
205
|
begin
|
205
|
-
user_acl =
|
206
|
+
user_acl = @tidy.json_file_to_hash(user_acl_path, symbolize_names: false)
|
206
207
|
rescue Errno::ENOENT
|
207
208
|
@tidy.ui.stdout.puts "REPAIRING: Replacing missing user acl for #{member[:user][:username]}."
|
208
209
|
@tidy.write_new_file(default_user_acl(member), user_acl_path, backup = false)
|
209
|
-
user_acl =
|
210
|
+
user_acl = @tidy.json_file_to_hash(user_acl_path, symbolize_names: false)
|
210
211
|
end
|
211
212
|
ensure_global_group_acls(user_acl_path)
|
212
213
|
actors_groups = acl_actors_groups(user_acl)
|
@@ -220,11 +221,11 @@ class Chef
|
|
220
221
|
@clients.each do |client|
|
221
222
|
client_acl_path = ::File.join(@tidy.org_acls_path(@org), "clients", "#{client[:name]}.json")
|
222
223
|
begin
|
223
|
-
client_acl =
|
224
|
+
client_acl = @tidy.json_file_to_hash(client_acl_path, symbolize_names: false)
|
224
225
|
rescue Errno::ENOENT
|
225
226
|
@tidy.ui.stdout.puts "REPAIRING: Replacing missing client acl for #{client[:name]} in #{client_acl_path}."
|
226
227
|
@tidy.write_new_file(default_client_acl(client[:name]), client_acl_path, backup = false)
|
227
|
-
client_acl =
|
228
|
+
client_acl = @tidy.json_file_to_hash(client_acl_path, symbolize_names: false)
|
228
229
|
end
|
229
230
|
ensure_client_read_acls(client_acl_path)
|
230
231
|
end
|
data/lib/chef/tidy_common.rb
CHANGED
@@ -12,61 +12,130 @@ class Chef
|
|
12
12
|
@backup_path = ::File.expand_path(backup_path)
|
13
13
|
end
|
14
14
|
|
15
|
+
#
|
16
|
+
# @return [Chef::Knife::UI]
|
17
|
+
#
|
15
18
|
def ui
|
16
19
|
@ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
|
17
20
|
end
|
18
21
|
|
22
|
+
# The path to the users directory in the backup
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
#
|
19
26
|
def users_path
|
20
27
|
@users_path ||= ::File.expand_path(::File.join(@backup_path, "users"))
|
21
28
|
end
|
22
29
|
|
30
|
+
# The path to the members.json file in the backup
|
31
|
+
#
|
32
|
+
# @param [String] org
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
#
|
23
36
|
def members_path(org)
|
24
37
|
::File.expand_path(::File.join(@backup_path, "organizations", org, "members.json"))
|
25
38
|
end
|
26
39
|
|
40
|
+
# The path to the invitations.json file in the backup
|
41
|
+
#
|
42
|
+
# @param [String] org
|
43
|
+
#
|
44
|
+
# @return [String]
|
45
|
+
#
|
27
46
|
def invitations_path(org)
|
28
47
|
::File.expand_path(::File.join(@backup_path, "organizations", org, "invitations.json"))
|
29
48
|
end
|
30
49
|
|
50
|
+
# The path to the clients directory in the backup
|
51
|
+
#
|
52
|
+
# @param [String] org
|
53
|
+
#
|
54
|
+
# @return [String]
|
55
|
+
#
|
31
56
|
def clients_path(org)
|
32
57
|
::File.expand_path(::File.join(@backup_path, "organizations", org, "clients"))
|
33
58
|
end
|
34
59
|
|
60
|
+
# The paths to each of the client json files in the backup
|
61
|
+
#
|
62
|
+
# @param [String] org
|
63
|
+
#
|
64
|
+
# @return [Array]
|
65
|
+
#
|
35
66
|
def client_names(org)
|
36
67
|
Dir[::File.join(clients_path(org), "*")].map { |dir| ::File.basename(dir, ".json") }
|
37
68
|
end
|
38
69
|
|
70
|
+
# The path to groups directory in the backup
|
71
|
+
#
|
72
|
+
# @param [String] org
|
73
|
+
#
|
74
|
+
# @return [String]
|
75
|
+
#
|
39
76
|
def groups_path(org)
|
40
77
|
::File.expand_path(::File.join(@backup_path, "organizations", org, "groups"))
|
41
78
|
end
|
42
79
|
|
80
|
+
# The path to acls directory in the backup
|
81
|
+
#
|
82
|
+
# @param [String] org
|
83
|
+
#
|
84
|
+
# @return [String]
|
85
|
+
#
|
43
86
|
def org_acls_path(org)
|
44
87
|
::File.expand_path(::File.join(@backup_path, "organizations", org, "acls"))
|
45
88
|
end
|
46
89
|
|
90
|
+
# The path to user_acls directory in the backup
|
91
|
+
#
|
92
|
+
# @return [String]
|
93
|
+
#
|
47
94
|
def user_acls_path
|
48
95
|
@user_acls_path ||= ::File.expand_path(::File.join(@backup_path, "user_acls"))
|
49
96
|
end
|
50
97
|
|
98
|
+
# The path to cookbooks directory in the backup
|
99
|
+
#
|
100
|
+
# @param [String] org
|
101
|
+
#
|
102
|
+
# @return [String]
|
103
|
+
#
|
51
104
|
def cookbooks_path(org)
|
52
105
|
::File.expand_path(::File.join(@backup_path, "organizations", org, "cookbooks"))
|
53
106
|
end
|
54
107
|
|
108
|
+
# The path to roles directory in the backup
|
109
|
+
#
|
110
|
+
# @param [String] org
|
111
|
+
#
|
112
|
+
# @return [String]
|
113
|
+
#
|
55
114
|
def roles_path(org)
|
56
115
|
::File.expand_path(::File.join(@backup_path, "organizations", org, "roles"))
|
57
116
|
end
|
58
117
|
|
118
|
+
# The path to the org directory in the backup
|
119
|
+
#
|
120
|
+
# @param [String] org
|
121
|
+
#
|
122
|
+
# @return [String]
|
123
|
+
#
|
59
124
|
def org_path(org)
|
60
125
|
::File.expand_path(::File.join(@backup_path, "organizations", org))
|
61
126
|
end
|
62
127
|
|
128
|
+
# generate a bogus, but valid email
|
129
|
+
#
|
130
|
+
# @return [String]
|
131
|
+
#
|
63
132
|
def unique_email
|
64
133
|
(0...8).map { (65 + rand(26)).chr }.join.downcase +
|
65
134
|
"@" + (0...8).map { (65 + rand(26)).chr }.join.downcase + ".com"
|
66
135
|
end
|
67
136
|
|
68
137
|
def save_user(user)
|
69
|
-
::File.open(::File.join(users_path, "#{user[
|
138
|
+
::File.open(::File.join(users_path, "#{user["username"]}.json"), "w+") do |f|
|
70
139
|
f.write(FFI_Yajl::Encoder.encode(user, pretty: true))
|
71
140
|
end
|
72
141
|
end
|
@@ -80,10 +149,62 @@ class Chef
|
|
80
149
|
end
|
81
150
|
end
|
82
151
|
|
152
|
+
# Read a json file and return a hash of parsed content with optional symbolized keys
|
153
|
+
#
|
154
|
+
# @param [String] path to file
|
155
|
+
# @param [double splat] options to pass FFI_Yajl::Parser.parse()
|
156
|
+
#
|
157
|
+
# @return [Hash] original json content as hash
|
158
|
+
#
|
159
|
+
# @example
|
160
|
+
# json_file_to_hash('/path/to/file.json', symbolize_names: true) => { foo: "bar" }
|
161
|
+
#
|
162
|
+
def json_file_to_hash(file_path, **options)
|
163
|
+
FFI_Yajl::Parser.parse(File.read(file_path), options)
|
164
|
+
rescue Errno::ENOENT, Errno::EACCES, FFI_Yajl::ParseError
|
165
|
+
puts "ERROR: unable to parse file: '#{file_path}'"
|
166
|
+
raise
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Determine the cookbook name from path
|
171
|
+
#
|
172
|
+
# @param [String] path The path of the cookbook.
|
173
|
+
#
|
174
|
+
# @return [String] The cookbook's name
|
175
|
+
#
|
176
|
+
# @example
|
177
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4') => 'chef-sugar'
|
178
|
+
#
|
83
179
|
def cookbook_name_from_path(path)
|
84
180
|
::File.basename(path, "-*")
|
85
181
|
end
|
86
182
|
|
183
|
+
#
|
184
|
+
# Determine the cookbook version from a path.
|
185
|
+
#
|
186
|
+
# @param [String] path The path of the cookbook.
|
187
|
+
#
|
188
|
+
# @return [String] The version of the cookbook.
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4') => '5.0.4'
|
192
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4/recipe/default.rb') => '5.0.4'
|
193
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4/files/cookbooks/default.rb') => '5.0.4'
|
194
|
+
#
|
195
|
+
def cookbook_version_from_path(path)
|
196
|
+
dirs = path.split(File::SEPARATOR)
|
197
|
+
|
198
|
+
until dirs.empty?
|
199
|
+
version_match = dirs[-1].match(/\d+\.\d+\.\d+/)
|
200
|
+
if dirs[-2] == "cookbooks" && version_match # we found the cookbook version not something that looks like one inside a cookbook path
|
201
|
+
return version_match.to_s
|
202
|
+
else
|
203
|
+
dirs.pop
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
87
208
|
def global_user_names
|
88
209
|
@global_user_names ||= Dir[::File.join(@backup_path, "users", "*")].map { |dir| ::File.basename(dir, ".json") }
|
89
210
|
end
|
data/lib/chef/tidy_server.rb
CHANGED
@@ -17,7 +17,7 @@ class Chef
|
|
17
17
|
|
18
18
|
def load_data
|
19
19
|
@tidy.ui.stdout.puts "INFO: Loading substitutions from #{file_path}"
|
20
|
-
@data =
|
20
|
+
@data = @tidy.json_file_to_hash(@file_path, symbolize_names: false)
|
21
21
|
rescue Errno::ENOENT
|
22
22
|
raise NoSubstitutionFile, file_path
|
23
23
|
end
|
@@ -28,14 +28,7 @@ class Chef
|
|
28
28
|
FileUtils.cp(bp, ::File.join(Dir.pwd, "substitutions.json"))
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
components = path.split(File::SEPARATOR)
|
33
|
-
name_version = components[components.index("cookbooks") + 1]
|
34
|
-
name_version.match(/\d+\.\d+\.\d+/).to_s
|
35
|
-
end
|
36
|
-
|
37
|
-
def revert
|
38
|
-
end
|
31
|
+
def revert; end
|
39
32
|
|
40
33
|
def sub_in_file(path, search, replace)
|
41
34
|
temp_file = Tempfile.new("tidy")
|
@@ -68,7 +61,7 @@ class Chef
|
|
68
61
|
@data[entry][glob].each do |substitution|
|
69
62
|
search = Regexp.new(substitution["pattern"])
|
70
63
|
replace = substitution["replace"].dup
|
71
|
-
replace.gsub!(/\!COOKBOOK_VERSION\!/) { |_m| "'" + cookbook_version_from_path(file) + "'" }
|
64
|
+
replace.gsub!(/\!COOKBOOK_VERSION\!/) { |_m| "'" + @tidy.cookbook_version_from_path(file) + "'" }
|
72
65
|
sub_in_file(file, search, replace)
|
73
66
|
end
|
74
67
|
end
|
data/lib/knife-tidy/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: knife-tidy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Miller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-06-11 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Report on stale Chef Server nodes and cookbooks and clean up data integrity
|
14
14
|
issues in a knife-ec-backup object based backup
|
@@ -19,6 +19,7 @@ extensions: []
|
|
19
19
|
extra_rdoc_files: []
|
20
20
|
files:
|
21
21
|
- LICENSE
|
22
|
+
- conf/substitutions.json.example
|
22
23
|
- lib/chef/knife/tidy_backup_clean.rb
|
23
24
|
- lib/chef/knife/tidy_base.rb
|
24
25
|
- lib/chef/knife/tidy_notify.rb
|
@@ -48,8 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
49
|
- !ruby/object:Gem::Version
|
49
50
|
version: '0'
|
50
51
|
requirements: []
|
51
|
-
|
52
|
-
rubygems_version: 2.7.7
|
52
|
+
rubygems_version: 3.0.3
|
53
53
|
signing_key:
|
54
54
|
specification_version: 4
|
55
55
|
summary: Report on stale Chef Server nodes and cookbooks and clean up data integrity
|