knife-tidy 2.0.1 → 2.0.6
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/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
|