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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23b1bd5cd9472c351eddcf5722b5883529ab52ff7d2823cd1f71ffd050ea382e
4
- data.tar.gz: 47d38da1aaea0f9ea819a3fdac00ed8736196d9429c0629a26f2d8562a4f6549
3
+ metadata.gz: c7aaf34102e27291f03892bfb4eddb450aaed46c5e615818e48a59c174c91e42
4
+ data.tar.gz: f482bbfb1182b581532ef643900c6f4814dc800ff42e02ff0a29b8936faed77b
5
5
  SHA512:
6
- metadata.gz: 564fbad7997899cc26a55b3c265edbd7a5e0be3aa518c86e4fb67b651e30a6e55311e0042ea2b0f7a009b1b1e0984894e54b6c23cd643d076097789c9a2e3c2a
7
- data.tar.gz: 3ef4230e711258c422cf14b9a53b15bd9797e917afc79bf1dee2763f5ee748b84d4ee02190e20fe70ba27d65a8a448f0a243229090ef5ca4d8c710d1c521f82c
6
+ metadata.gz: a5192215cbd8341aef601e71bf6c42bb2eb5d19f652570734ac192151941d7b954da8d0fe5c504ea67b599772d09ea12da77d6d782f54e2c967c6c12fc9cef0d
7
+ data.tar.gz: 75b37b5663f0558791ad1a52b5281d84197e1def8ed2c15103896165a781b4a07f1ac4556f5d266e35fb662d76218e6be0ccf413d569e0b7b776075e5a5aca27
@@ -0,0 +1,10 @@
1
+ {
2
+ "your-problem-descriptor":{
3
+ "organizations/*/cookbooks/*/metadata.rb":[
4
+ {
5
+ "pattern":"^version .*GO_PIPELINE_LABEL",
6
+ "replace":"version !COOKBOOK_VERSION!"
7
+ }
8
+ ]
9
+ }
10
+ }
@@ -1,15 +1,15 @@
1
- require "chef/knife/tidy_base"
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/cookbook_loader"
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
- require "chef/tidy_substitutions"
12
- require "chef/tidy_acls"
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 = FFI_Yajl::Parser.parse(::File.read(::File.join(tidy.users_path, "#{user}.json")), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(json_path), symbolize_names: false)
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
- cl = Chef::CookbookLoader.new(tidy.cookbooks_path(org))
165
- for_each_cookbook_basename(org) do |cookbook|
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.load_cookbook(cookbook)
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, cookbook)
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
- Dir[::File.join(tidy.cookbooks_path(org), "#{cookbook}*")].each do |cb|
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 = s.cookbook_version_from_path(file)
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 = FFI_Yajl::Parser.parse(::File.read(json_path), symbolize_names: false)
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
- components = cookbook_path.split(File::SEPARATOR)
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 = FFI_Yajl::Parser.parse(::File.read(role_path), symbolize_names: false)
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(FFI_Yajl::Parser.parse(::File.read(role_path), symbolize_names: false))
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 = FFI_Yajl::Parser.parse(::File.read(clients_group_path), symbolize_names: false)
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['clients'].length - tidy.client_names(org).length).abs} missing clients into #{org}'s client group file #{clients_group_path}"
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 = FFI_Yajl::Parser.parse(::File.read(invite_file), symbolize_names: false)
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?
@@ -24,8 +24,8 @@ class Chef
24
24
  def self.included(includer)
25
25
  includer.class_eval do
26
26
  deps do
27
- require "chef/tidy_server"
28
- require "chef/tidy_common"
27
+ require_relative "../tidy_server"
28
+ require_relative "../tidy_common"
29
29
  end
30
30
 
31
31
  option :org_list,
@@ -1,4 +1,4 @@
1
- require "chef/knife/tidy_base"
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
- 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)"
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
- 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)"
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
- 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)"
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
- short: "-u SMTP_USERNAME",
33
- long: "--smtp_username SMTP_USERNAME",
34
- description: "SMTP Username to be used for emailling reports to organization admins"
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
- long: "--smtp_password SMTP_PASSWORD",
38
- description: "SMTP Password to be used for emailling reports to organization admins"
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
- long: "--smtp_from SMTP_FROM",
42
- description: "SMTP From address to be used for emailling reports to organization admins"
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
- 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)"
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('|')})").captures.first }.uniq
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] = FFI_Yajl::Parser.parse(json_string)
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('<br>')}</td></tr>" }.join("\n")
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]['_stale_nodes.json']['threshold_days']} days.<p><table border='1' cellpadding='1' cellspacing='0'>"
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
- require "chef/knife/tidy_base"
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 = FFI_Yajl::Parser.parse(::File.read(unused_cookbooks_file), symbolize_names: true)
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 = FFI_Yajl::Parser.parse(::File.read(stale_nodes_file), symbolize_names: true)
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
- require "chef/knife/tidy_base"
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 cleanup action will be taken for #{org}."
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
- if !node["chef_packages"]
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|
@@ -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(FFI_Yajl::Parser.parse(::File.read(user), symbolize_names: true))
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 = FFI_Yajl::Parser.parse(::File.read(@tidy.members_path(@org)), symbolize_names: true)
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(FFI_Yajl::Parser.parse(::File.read(client), symbolize_names: true))
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(FFI_Yajl::Parser.parse(::File.read(group), symbolize_names: true))
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 = FFI_Yajl::Parser.parse(::File.read(acl_file), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(acl_file), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(acl_file), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(acl_file), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(user_acl_path), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(user_acl_path), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(client_acl_path), symbolize_names: false)
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 = FFI_Yajl::Parser.parse(::File.read(client_acl_path), symbolize_names: false)
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
@@ -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['username']}.json"), "w+") do |f|
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
@@ -7,7 +7,7 @@ class Chef
7
7
  end
8
8
 
9
9
  def self.from_chef_server_url(url)
10
- url = url.gsub(/\/organizations\/+[^\/]+\/*$/, "")
10
+ url = url.gsub(%r{/organizations/+[^/]+/*$}, "")
11
11
  Chef::Server.new(url)
12
12
  end
13
13
  end
@@ -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 = FFI_Yajl::Parser.parse(::File.read(@file_path), symbolize_names: false)
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 cookbook_version_from_path(path)
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
@@ -1,4 +1,4 @@
1
1
  module KnifeTidy
2
- VERSION = "2.0.1".freeze
2
+ VERSION = "2.0.15".freeze
3
3
  MAJOR, MINOR, TINY = VERSION.split(".")
4
4
  end
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.1
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: 2019-02-07 00:00:00.000000000 Z
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
- rubyforge_project:
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