knife-tidy 2.0.1 → 2.0.15

Sign up to get free protection for your applications and to get access to all the features.
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