knife-tidy 2.0.1 → 2.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/chef/knife/tidy_backup_clean.rb +5 -6
- data/lib/chef/knife/tidy_notify.rb +29 -29
- data/lib/chef/knife/tidy_server_clean.rb +2 -0
- data/lib/chef/knife/tidy_server_report.rb +5 -3
- data/lib/chef/tidy_acls.rb +1 -0
- data/lib/chef/tidy_common.rb +105 -1
- data/lib/chef/tidy_server.rb +1 -1
- data/lib/chef/tidy_substitutions.rb +2 -9
- data/lib/knife-tidy/version.rb +1 -1
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 562e7dd8fc15d4b707b8b3d70c0bc54a3226f9d54d8bcc72ef4cf5436320fc1a
|
4
|
+
data.tar.gz: 61a42597e3340788e0f4040b5c251c5fce14622afadb077048886936e445815f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c475ece3e2a94d08a521f4262820e88e5103d8e1cc3cc7273dbd6c1ce639d122e2c00904a46fe71ddec0eefee87aa567f7de574fd965764cebe70135f65d7d0
|
7
|
+
data.tar.gz: 33c2841cfc1d1c51db87a06c7a61ffd26c630096962e0ce70aac4df6d9ac234818e82c0b7ab2894a0a440fd804cb5a15a05fdbe9d691dc84ca0f1b762157d5b7
|
@@ -194,7 +194,7 @@ class Chef
|
|
194
194
|
Dir[::File.join(tidy.backup_path, "organizations/*/cookbooks/chef-sugar*/metadata.rb")].each do |file|
|
195
195
|
ui.stdout.puts "INFO: Searching for known chef-sugar problems when uploading."
|
196
196
|
s = Chef::TidySubstitutions.new(nil, tidy)
|
197
|
-
version =
|
197
|
+
version = tidy.cookbook_version_from_path(::File.dirname(file))
|
198
198
|
patterns = [
|
199
199
|
{
|
200
200
|
search: "^require .*/lib/chef/sugar/version",
|
@@ -222,7 +222,8 @@ class Chef
|
|
222
222
|
Chef::TidySubstitutions.new(nil, tidy).sub_in_file(
|
223
223
|
::File.join(cookbook_path, "metadata.rb"),
|
224
224
|
Regexp.new("^depends +['\"]#{name}['\"]"),
|
225
|
-
"# depends '#{name}' # knife-tidy was here"
|
225
|
+
"# depends '#{name}' # knife-tidy was here"
|
226
|
+
)
|
226
227
|
end
|
227
228
|
end
|
228
229
|
|
@@ -279,9 +280,7 @@ class Chef
|
|
279
280
|
|
280
281
|
def create_minimal_metadata(cookbook_path)
|
281
282
|
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
|
283
|
+
version = tidy.cookbook_version_from_path(cookbook_path)
|
285
284
|
metadata = {}
|
286
285
|
metadata["name"] = name
|
287
286
|
metadata["version"] = version
|
@@ -398,7 +397,7 @@ class Chef
|
|
398
397
|
existing_group_data = FFI_Yajl::Parser.parse(::File.read(clients_group_path), symbolize_names: false)
|
399
398
|
existing_group_data["clients"] = [] unless existing_group_data.key?("clients")
|
400
399
|
if existing_group_data["clients"].length != tidy.client_names(org).length
|
401
|
-
ui.stdout.puts "REPAIRING: Adding #{(existing_group_data[
|
400
|
+
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
401
|
existing_group_data["clients"] = (existing_group_data["clients"] + tidy.client_names(org)).uniq
|
403
402
|
::File.open(clients_group_path, "w") do |f|
|
404
403
|
f.write(Chef::JSONCompat.to_json_pretty(existing_group_data))
|
@@ -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
|
@@ -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
|
@@ -89,6 +89,7 @@ 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
94
|
unused_cookbooks = FFI_Yajl::Parser.parse(::File.read(unused_cookbooks_file), symbolize_names: true)
|
94
95
|
unused_cookbooks.keys.each do |cookbook|
|
@@ -115,6 +116,7 @@ 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
121
|
stale_nodes = FFI_Yajl::Parser.parse(::File.read(stale_nodes_file), symbolize_names: true)
|
120
122
|
stale_nodes[:list].each do |node|
|
@@ -43,7 +43,7 @@ class Chef
|
|
43
43
|
nodes = nodes_list(org)
|
44
44
|
db_nodes = rest.get("/organizations/#{org}/nodes")
|
45
45
|
unless nodes.length == db_nodes.length
|
46
|
-
ood_message = "Search index is out of date! No
|
46
|
+
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
47
|
ui.error(ood_message)
|
48
48
|
action_needed(ood_message, server_warnings_file_path)
|
49
49
|
next
|
@@ -51,7 +51,7 @@ class Chef
|
|
51
51
|
|
52
52
|
nodes.each do |node|
|
53
53
|
# If the node hasn't checked in.
|
54
|
-
|
54
|
+
unless node["chef_packages"]
|
55
55
|
# If the node is under an hour old.
|
56
56
|
if (Time.now.to_i - node["ohai_time"].to_i) < 3600
|
57
57
|
unconverged_recent_nodes << node["name"]
|
@@ -207,8 +207,9 @@ class Chef
|
|
207
207
|
else
|
208
208
|
versions_not_satisfied.push(v)
|
209
209
|
end
|
210
|
+
|
210
211
|
if v == cb_list[cb].last
|
211
|
-
ui.warn("Pin of #{cb} #{version} not satisfied by current versions of cookbook: [#{versions_not_satisfied.join(
|
212
|
+
ui.warn("Pin of #{cb} #{version} not satisfied by current versions of cookbook: [#{versions_not_satisfied.join(", ")}]")
|
212
213
|
end
|
213
214
|
end
|
214
215
|
else
|
@@ -221,6 +222,7 @@ class Chef
|
|
221
222
|
pins.each do |cb, versions|
|
222
223
|
versions.each do |version|
|
223
224
|
next if version == "<= 0.0.0"
|
225
|
+
|
224
226
|
if used_cookbooks[cb]
|
225
227
|
# This pinned cookbook is in the used list, now check for a matching version.
|
226
228
|
used_cookbooks[cb].each do |v|
|
data/lib/chef/tidy_acls.rb
CHANGED
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,45 @@ class Chef
|
|
80
149
|
end
|
81
150
|
end
|
82
151
|
|
152
|
+
#
|
153
|
+
# Determine the cookbook name from path
|
154
|
+
#
|
155
|
+
# @param [String] path The path of the cookbook.
|
156
|
+
#
|
157
|
+
# @return [String] The cookbook's name
|
158
|
+
#
|
159
|
+
# @example
|
160
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4') => 'chef-sugar'
|
161
|
+
#
|
83
162
|
def cookbook_name_from_path(path)
|
84
163
|
::File.basename(path, "-*")
|
85
164
|
end
|
86
165
|
|
166
|
+
#
|
167
|
+
# Determine the cookbook version from a path.
|
168
|
+
#
|
169
|
+
# @param [String] path The path of the cookbook.
|
170
|
+
#
|
171
|
+
# @return [String] The version of the cookbook.
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4') => '5.0.4'
|
175
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4/recipe/default.rb') => '5.0.4'
|
176
|
+
# cookbook_version_from_path('/data/chef_backup/snapshots/20191008040001/organizations/myorg/cookbooks/chef-sugar-5.0.4/files/cookbooks/default.rb') => '5.0.4'
|
177
|
+
#
|
178
|
+
def cookbook_version_from_path(path)
|
179
|
+
dirs = path.split(File::SEPARATOR)
|
180
|
+
|
181
|
+
until dirs.empty?
|
182
|
+
version_match = dirs[-1].match(/\d+\.\d+\.\d+/)
|
183
|
+
if dirs[-2] == "cookbooks" && version_match # we found the cookbook version not something that looks like one inside a cookbook path
|
184
|
+
return version_match.to_s
|
185
|
+
else
|
186
|
+
dirs.pop
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
87
191
|
def global_user_names
|
88
192
|
@global_user_names ||= Dir[::File.join(@backup_path, "users", "*")].map { |dir| ::File.basename(dir, ".json") }
|
89
193
|
end
|
data/lib/chef/tidy_server.rb
CHANGED
@@ -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.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Miller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-12-04 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
|
@@ -48,8 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
48
|
- !ruby/object:Gem::Version
|
49
49
|
version: '0'
|
50
50
|
requirements: []
|
51
|
-
|
52
|
-
rubygems_version: 2.7.7
|
51
|
+
rubygems_version: 3.0.3
|
53
52
|
signing_key:
|
54
53
|
specification_version: 4
|
55
54
|
summary: Report on stale Chef Server nodes and cookbooks and clean up data integrity
|