knife-tidy 1.2.0 → 2.0.0

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: fe2a778de3020252562b8788aca9ad4c2a84c50621b1659c867b7b5b5db89d5e
4
- data.tar.gz: b305e31422e5cf415021bed9a0e3251550d6962b5ed0ba75a98c213741e2f077
3
+ metadata.gz: b559f18636e7cc22f399f44e5f3efd3e3c43d84c1fc5c34a1291ab6dcf8e1a6c
4
+ data.tar.gz: 067e18dcec7889510d0b95b443fa54d3f2e38577bdcffe71a01e1199ed956c03
5
5
  SHA512:
6
- metadata.gz: e0edc9e58ccd32ba959e0cdc1f1200b937459f0daad9547f302f4878da2d889a626b0dcedd8b90e8515e0c425f298ad3daa2a0169ba62530865dd998a85a69a0
7
- data.tar.gz: 980af3d17bb27eb7cf060e9e56c0f8419a9e3f557b78702ed3ef05106cf2dbf4323f025bbdf3cc76ae24bc50d71e98c51c45b9b99892ffd9c7adc7117ced2884
6
+ metadata.gz: d08512efe742bfe15cb2f52ed36e9f68d0e6aa26726cedd5649fb46917538e80bf8a9b1b264bb22d7e5c5706d2d7094c0d6ea320cbe39689605e30baa184be2e
7
+ data.tar.gz: 62b1d8f9465d6abe63e33996894f64f7db08eacf0cdff7801abb5751c0fcef3950458ed7ebbbc5f369b03e7e9855176a488ae737f7d4b50252011fe6525dfba6
@@ -1,35 +1,35 @@
1
- require 'chef/knife/tidy_base'
1
+ require "chef/knife/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'
8
- require 'chef/cookbook/metadata'
9
- require 'chef/role'
10
- require 'chef/run_list'
11
- require 'chef/tidy_substitutions'
12
- require 'chef/tidy_acls'
13
- require 'ffi_yajl'
14
- require 'fileutils'
15
- require 'securerandom'
7
+ require "chef/cookbook_loader"
8
+ require "chef/cookbook/metadata"
9
+ require "chef/role"
10
+ require "chef/run_list"
11
+ require "chef/tidy_substitutions"
12
+ require "chef/tidy_acls"
13
+ require "ffi_yajl"
14
+ require "fileutils"
15
+ require "securerandom"
16
16
  end
17
17
 
18
- banner 'knife tidy backup clean (options)'
18
+ banner "knife tidy backup clean (options)"
19
19
 
20
20
  include Knife::TidyBase
21
21
 
22
22
  option :backup_path,
23
- long: '--backup-path path/to/backup',
24
- description: 'The path to the knife-ec-backup backup directory'
23
+ long: "--backup-path path/to/backup",
24
+ description: "The path to the knife-ec-backup backup directory"
25
25
 
26
26
  option :gsub_file,
27
- long: '--gsub-file path/to/gsub/file',
28
- description: 'The path to the file used for substitutions. If non-existant, a boiler plate one will be created.'
27
+ long: "--gsub-file path/to/gsub/file",
28
+ description: "The path to the file used for substitutions. If non-existant, a boiler plate one will be created."
29
29
 
30
30
  option :gen_gsub,
31
- long: '--gen-gsub',
32
- description: 'Generate a new boiler plate global substitutions file: \'substitutions.json\'.'
31
+ long: "--gen-gsub",
32
+ description: "Generate a new boiler plate global substitutions file: 'substitutions.json'."
33
33
 
34
34
  def run
35
35
  FileUtils.rm_f(action_needed_file_path)
@@ -40,7 +40,7 @@ class Chef
40
40
  end
41
41
 
42
42
  unless config[:backup_path] && ::File.directory?(config[:backup_path])
43
- ui.error 'Must specify valid --backup-path'
43
+ ui.error "Must specify valid --backup-path"
44
44
  exit 1
45
45
  end
46
46
 
@@ -73,25 +73,25 @@ class Chef
73
73
  def validate_user_emails
74
74
  emails_seen = []
75
75
  tidy.global_user_names.each do |user|
76
- email = ''
76
+ email = ""
77
77
  ui.stdout.puts "INFO: Validating #{user}"
78
78
  the_user = FFI_Yajl::Parser.parse(::File.read(::File.join(tidy.users_path, "#{user}.json")), symbolize_names: false)
79
- if the_user.key?('email') && the_user['email'].match(/\A[^@\s]+@[^@\s]+\z/)
80
- if emails_seen.include?(the_user['email'])
79
+ if the_user.key?("email") && the_user["email"].match(/\A[^@\s]+@[^@\s]+\z/)
80
+ if emails_seen.include?(the_user["email"])
81
81
  ui.stdout.puts "REPAIRING: Already saw #{user}'s email, creating a unique one."
82
82
  email = tidy.unique_email
83
83
  new_user = the_user.dup
84
- new_user['email'] = email
84
+ new_user["email"] = email
85
85
  tidy.save_user(new_user)
86
86
  emails_seen.push(email)
87
87
  else
88
- emails_seen.push(the_user['email'])
88
+ emails_seen.push(the_user["email"])
89
89
  end
90
90
  else
91
91
  ui.stdout.puts "REPAIRING: User #{user} does not have a valid email, creating a unique one."
92
92
  email = tidy.unique_email
93
93
  new_user = the_user.dup
94
- new_user['email'] = email
94
+ new_user["email"] = email
95
95
  tidy.save_user(new_user)
96
96
  emails_seen.push(email)
97
97
  end
@@ -108,9 +108,9 @@ class Chef
108
108
  unless org_object.keys.count == 3 # cheapo, maybe expect the exact names?
109
109
  ui.stdout.puts "REPAIRING: org object for #{org} contains extra/missing fields. Fixing that for you"
110
110
  # quick/dirty attempt at fixing any of the required fields in case they're nil
111
- good_name = org_object['name'] || org
112
- good_full_name = org_object['full_name'] || org
113
- good_guid = org_object['guid'] || SecureRandom.uuid.delete('-')
111
+ good_name = org_object["name"] || org
112
+ good_full_name = org_object["full_name"] || org
113
+ good_guid = org_object["guid"] || SecureRandom.uuid.delete("-")
114
114
  fixed_org_object = { name: good_name, full_name: good_full_name, guid: good_guid }
115
115
 
116
116
  write_org_object(org, fixed_org_object)
@@ -118,14 +118,14 @@ class Chef
118
118
  end
119
119
 
120
120
  def load_org_object(org)
121
- JSON.parse(File.read(File.join(tidy.org_path(org), 'org.json')))
121
+ JSON.parse(File.read(File.join(tidy.org_path(org), "org.json")))
122
122
  rescue Errno::ENOENT, JSON::ParserError
123
123
  ui.stdout.puts "REPAIRING: org object for organization #{org} is missing or corrupt. Generating a new one"
124
- return { name: org, full_name: org, guid: SecureRandom.uuid.delete('-') }
124
+ { name: org, full_name: org, guid: SecureRandom.uuid.delete("-") }
125
125
  end
126
126
 
127
127
  def write_org_object(org, org_object)
128
- File.write(File.join(tidy.org_path(org), 'org.json'), JSON.pretty_generate(org_object))
128
+ File.write(File.join(tidy.org_path(org), "org.json"), JSON.pretty_generate(org_object))
129
129
  end
130
130
 
131
131
  def add_cookbook_name_to_metadata(cookbook_name, rb_path)
@@ -133,13 +133,13 @@ class Chef
133
133
  content = IO.readlines(rb_path)
134
134
  new_content = content.reject { |line| line =~ /^name\s+/ }
135
135
  name_field = "name '#{cookbook_name}'\n"
136
- IO.write rb_path, name_field + new_content.join('')
136
+ IO.write rb_path, name_field + new_content.join("")
137
137
  end
138
138
 
139
139
  def fix_cookbook_names(org)
140
140
  for_each_cookbook_path(org) do |cookbook_path|
141
- rb_path = ::File.join(cookbook_path, 'metadata.rb')
142
- json_path = ::File.join(cookbook_path, 'metadata.json')
141
+ rb_path = ::File.join(cookbook_path, "metadata.rb")
142
+ json_path = ::File.join(cookbook_path, "metadata.json")
143
143
  # next unless ::File.exist?(rb_path)
144
144
  cookbook_name = tidy.cookbook_name_from_path(cookbook_path)
145
145
  if ::File.exist?(rb_path)
@@ -148,10 +148,10 @@ class Chef
148
148
  else
149
149
  if ::File.exist?(json_path)
150
150
  metadata = FFI_Yajl::Parser.parse(::File.read(json_path), symbolize_names: false)
151
- if metadata['name'] != cookbook_name
152
- metadata['name'] = cookbook_name
151
+ if metadata["name"] != cookbook_name
152
+ metadata["name"] = cookbook_name
153
153
  ui.stdout.puts "REPAIRING: Correcting `name` in #{json_path}`"
154
- ::File.open(json_path, 'w') do |f|
154
+ ::File.open(json_path, "w") do |f|
155
155
  f.write(Chef::JSONCompat.to_json_pretty(metadata))
156
156
  end
157
157
  end
@@ -176,7 +176,7 @@ class Chef
176
176
  end
177
177
 
178
178
  def broken_cookooks_add(org, cookbook)
179
- broken_path = ::File.join(tidy.org_path(org), 'cookbooks.broken')
179
+ broken_path = ::File.join(tidy.org_path(org), "cookbooks.broken")
180
180
  FileUtils.mkdir(broken_path) unless ::File.directory?(broken_path)
181
181
  Dir[::File.join(tidy.cookbooks_path(org), "#{cookbook}*")].each do |cb|
182
182
  FileUtils.mv(cb, broken_path, verbose: true, force: true)
@@ -191,17 +191,17 @@ class Chef
191
191
  end
192
192
 
193
193
  def fix_chef_sugar_metadata
194
- Dir[::File.join(tidy.backup_path, 'organizations/*/cookbooks/chef-sugar*/metadata.rb')].each do |file|
195
- ui.stdout.puts 'INFO: Searching for known chef-sugar problems when uploading.'
194
+ Dir[::File.join(tidy.backup_path, "organizations/*/cookbooks/chef-sugar*/metadata.rb")].each do |file|
195
+ ui.stdout.puts "INFO: Searching for known chef-sugar problems when uploading."
196
196
  s = Chef::TidySubstitutions.new(nil, tidy)
197
197
  version = s.cookbook_version_from_path(file)
198
198
  patterns = [
199
199
  {
200
- search: '^require .*/lib/chef/sugar/version',
200
+ search: "^require .*/lib/chef/sugar/version",
201
201
  replace: "# require File.expand_path('../lib/chef/sugar/version', *__FILE__)",
202
202
  },
203
203
  {
204
- search: '^version *Chef::Sugar::VERSION',
204
+ search: "^version *Chef::Sugar::VERSION",
205
205
  replace: "version '#{version}'",
206
206
  },
207
207
  ]
@@ -214,46 +214,46 @@ class Chef
214
214
  def fix_self_dependencies(org)
215
215
  for_each_cookbook_path(org) do |cookbook_path|
216
216
  name = tidy.cookbook_name_from_path(cookbook_path)
217
- md_path = ::File.join(cookbook_path, 'metadata.rb')
217
+ md_path = ::File.join(cookbook_path, "metadata.rb")
218
218
  unless ::File.exist?(md_path)
219
219
  ui.stdout.puts "INFO: No metadata.rb in #{cookbook_path} - skipping"
220
220
  next
221
221
  end
222
222
  Chef::TidySubstitutions.new(nil, tidy).sub_in_file(
223
- ::File.join(cookbook_path, 'metadata.rb'),
223
+ ::File.join(cookbook_path, "metadata.rb"),
224
224
  Regexp.new("^depends +['\"]#{name}['\"]"),
225
225
  "# depends '#{name}' # knife-tidy was here")
226
226
  end
227
227
  end
228
228
 
229
229
  def fix_metadata_fields(cookbook_path)
230
- json_path = ::File.join(cookbook_path, 'metadata.json')
230
+ json_path = ::File.join(cookbook_path, "metadata.json")
231
231
  metadata = FFI_Yajl::Parser.parse(::File.read(json_path), symbolize_names: false)
232
232
  md = metadata.dup
233
233
  metadata.each_pair do |key, value|
234
234
  if value.nil?
235
235
  ui.stdout.puts "REPAIRING: Fixing null value for key #{key} in #{json_path}"
236
- md[key] = 'default value'
236
+ md[key] = "default value"
237
237
  end
238
238
  end
239
- if metadata.key?('platforms')
240
- metadata['platforms'].each_pair do |key, value|
239
+ if metadata.key?("platforms")
240
+ metadata["platforms"].each_pair do |key, value|
241
241
  # platform key cannot contain comma delimited values
242
- md['platforms'].delete(key) if key =~ /,/
242
+ md["platforms"].delete(key) if key =~ /,/
243
243
  if value.is_a?(Array) && value.empty?
244
244
  ui.stdout.puts "REPAIRING: Fixing empty platform key for for key #{key} in #{json_path}"
245
- md['platforms'][key] = '>= 0.0.0'
245
+ md["platforms"][key] = ">= 0.0.0"
246
246
  end
247
247
  end
248
248
  end
249
- ::File.open(json_path, 'w') do |f|
249
+ ::File.open(json_path, "w") do |f|
250
250
  f.write(Chef::JSONCompat.to_json_pretty(md))
251
251
  end
252
252
  end
253
253
 
254
254
  def generate_metadata_from_file(cookbook, path)
255
- md_path = ::File.join(path, 'metadata.rb')
256
- json_path = ::File.join(path, 'metadata.json')
255
+ md_path = ::File.join(path, "metadata.rb")
256
+ json_path = ::File.join(path, "metadata.json")
257
257
  if !::File.exist?(md_path) && !::File.exist?(json_path)
258
258
  create_minimal_metadata(path)
259
259
  end
@@ -265,8 +265,8 @@ class Chef
265
265
  md = Chef::Cookbook::Metadata.new
266
266
  md.name(cookbook)
267
267
  md.from_file(md_path)
268
- json_file = ::File.join(path, 'metadata.json')
269
- ::File.open(json_file, 'w') do |f|
268
+ json_file = ::File.join(path, "metadata.json")
269
+ ::File.open(json_file, "w") do |f|
270
270
  f.write(Chef::JSONCompat.to_json_pretty(md))
271
271
  end
272
272
  rescue Exceptions::ObsoleteDependencySyntax, Exceptions::InvalidVersionConstraint => e
@@ -280,18 +280,18 @@ class Chef
280
280
  def create_minimal_metadata(cookbook_path)
281
281
  name = tidy.cookbook_name_from_path(cookbook_path)
282
282
  components = cookbook_path.split(File::SEPARATOR)
283
- name_version = components[components.index('cookbooks') + 1]
283
+ name_version = components[components.index("cookbooks") + 1]
284
284
  version = name_version.match(/\d+\.\d+\.\d+/).to_s
285
285
  metadata = {}
286
- metadata['name'] = name
287
- metadata['version'] = version
288
- metadata['description'] = 'the description'
289
- metadata['long_description'] = 'the long description'
290
- metadata['maintainer'] = 'the maintainer'
291
- metadata['maintainer_email'] = 'the maintainer email'
292
- rb_file = ::File.join(cookbook_path, 'metadata.rb')
286
+ metadata["name"] = name
287
+ metadata["version"] = version
288
+ metadata["description"] = "the description"
289
+ metadata["long_description"] = "the long description"
290
+ metadata["maintainer"] = "the maintainer"
291
+ metadata["maintainer_email"] = "the maintainer email"
292
+ rb_file = ::File.join(cookbook_path, "metadata.rb")
293
293
  ui.stdout.puts "REPAIRING: no metadata files exist for #{cookbook_path}, creating #{rb_file}"
294
- ::File.open(rb_file, 'w') do |f|
294
+ ::File.open(rb_file, "w") do |f|
295
295
  metadata.each_pair do |key, value|
296
296
  f.write("#{key} '#{value}'\n")
297
297
  end
@@ -306,15 +306,15 @@ class Chef
306
306
 
307
307
  def orgs
308
308
  @orgs ||= if config[:org_list]
309
- config[:org_list].split(',')
309
+ config[:org_list].split(",")
310
310
  else
311
- Dir[::File.join(tidy.backup_path, 'organizations', '*')].map { |dir| ::File.basename(dir) }
311
+ Dir[::File.join(tidy.backup_path, "organizations", "*")].map { |dir| ::File.basename(dir) }
312
312
  end
313
313
  end
314
314
 
315
315
  def for_each_cookbook_basename(org)
316
316
  cookbooks_seen = []
317
- Dir[::File.join(tidy.cookbooks_path(org), '**-**')].each do |cookbook|
317
+ Dir[::File.join(tidy.cookbooks_path(org), "**-**")].each do |cookbook|
318
318
  name = tidy.cookbook_name_from_path(cookbook)
319
319
  unless cookbooks_seen.include?(name)
320
320
  cookbooks_seen.push(name)
@@ -324,29 +324,29 @@ class Chef
324
324
  end
325
325
 
326
326
  def for_each_cookbook_path(org)
327
- Dir[::File.join(tidy.cookbooks_path(org), '**')].each do |cookbook|
327
+ Dir[::File.join(tidy.cookbooks_path(org), "**")].each do |cookbook|
328
328
  yield cookbook
329
329
  end
330
330
  end
331
331
 
332
332
  def action_needed_file_path
333
- ::File.expand_path('knife-tidy-actions-needed.txt')
333
+ ::File.expand_path("knife-tidy-actions-needed.txt")
334
334
  end
335
335
 
336
336
  def action_needed(msg)
337
- ::File.open(action_needed_file_path, 'a') do |f|
337
+ ::File.open(action_needed_file_path, "a") do |f|
338
338
  f.write(msg + "\n")
339
339
  end
340
340
  end
341
341
 
342
342
  def write_role(path, role)
343
- ::File.open(path, 'w') do |f|
343
+ ::File.open(path, "w") do |f|
344
344
  f.write(Chef::JSONCompat.to_json_pretty(role))
345
345
  end
346
346
  end
347
347
 
348
348
  def for_each_role(org)
349
- Dir[::File.join(tidy.roles_path(org), '*.json')].each do |role|
349
+ Dir[::File.join(tidy.roles_path(org), "*.json")].each do |role|
350
350
  yield role
351
351
  end
352
352
  end
@@ -355,22 +355,22 @@ class Chef
355
355
  the_role = FFI_Yajl::Parser.parse(::File.read(role_path), symbolize_names: false)
356
356
  new_role = the_role.clone
357
357
  rl = Chef::RunList.new
358
- new_role['run_list'] = []
359
- the_role['run_list'].each do |item|
358
+ new_role["run_list"] = []
359
+ the_role["run_list"].each do |item|
360
360
  begin
361
361
  rl << item
362
- new_role['run_list'].push(item)
362
+ new_role["run_list"].push(item)
363
363
  rescue ArgumentError
364
364
  ui.stdout.puts "REPAIRING: Invalid Recipe Item: #{item} in run_list from #{role_path}"
365
365
  end
366
366
  end
367
- if the_role.key?('env_run_lists')
368
- the_role['env_run_lists'].each_pair do |key, value|
369
- new_role['env_run_lists'][key] = []
367
+ if the_role.key?("env_run_lists")
368
+ the_role["env_run_lists"].each_pair do |key, value|
369
+ new_role["env_run_lists"][key] = []
370
370
  value.each do |item|
371
371
  begin
372
372
  rl << item
373
- new_role['env_run_lists'][key].push(item)
373
+ new_role["env_run_lists"][key].push(item)
374
374
  rescue ArgumentError
375
375
  ui.stdout.puts "REPAIRING: Invalid Recipe Item: #{item} in env_run_lists #{key} from #{role_path}"
376
376
  end
@@ -394,13 +394,13 @@ class Chef
394
394
 
395
395
  def validate_clients_group(org)
396
396
  ui.stdout.puts "INFO: validating all clients for org #{org} exist in clients group"
397
- clients_group_path = ::File.join(tidy.groups_path(org), 'clients.json')
397
+ clients_group_path = ::File.join(tidy.groups_path(org), "clients.json")
398
398
  existing_group_data = FFI_Yajl::Parser.parse(::File.read(clients_group_path), symbolize_names: false)
399
- existing_group_data['clients'] = [] unless existing_group_data.key?('clients')
400
- if existing_group_data['clients'].length != tidy.client_names(org).length
399
+ existing_group_data["clients"] = [] unless existing_group_data.key?("clients")
400
+ if existing_group_data["clients"].length != tidy.client_names(org).length
401
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}"
402
- existing_group_data['clients'] = (existing_group_data['clients'] + tidy.client_names(org)).uniq
403
- ::File.open(clients_group_path, 'w') do |f|
402
+ existing_group_data["clients"] = (existing_group_data["clients"] + tidy.client_names(org)).uniq
403
+ ::File.open(clients_group_path, "w") do |f|
404
404
  f.write(Chef::JSONCompat.to_json_pretty(existing_group_data))
405
405
  end
406
406
  end
@@ -412,16 +412,16 @@ class Chef
412
412
  invitations = FFI_Yajl::Parser.parse(::File.read(invite_file), symbolize_names: false)
413
413
  invitations_new = []
414
414
  invitations.each do |invite|
415
- if invite['username'].nil?
415
+ if invite["username"].nil?
416
416
  ui.stdout.puts "REPAIRING: Dropping corrupt invitations for #{org} in file #{invite_file}"
417
417
  else
418
418
  invite_hash = {}
419
- invite_hash['id'] = invite['id']
420
- invite_hash['username'] = invite['username']
419
+ invite_hash["id"] = invite["id"]
420
+ invite_hash["username"] = invite["username"]
421
421
  invitations_new.push(invite_hash)
422
422
  end
423
423
  end
424
- ::File.open(invite_file, 'w') do |f|
424
+ ::File.open(invite_file, "w") do |f|
425
425
  f.write(Chef::JSONCompat.to_json_pretty(invitations_new))
426
426
  end
427
427
  end
@@ -15,8 +15,8 @@
15
15
  # limitations under the License.
16
16
  #
17
17
 
18
- require 'chef/knife'
19
- require 'chef/server_api'
18
+ require "chef/knife"
19
+ require "chef/server_api"
20
20
 
21
21
  class Chef
22
22
  class Knife
@@ -24,19 +24,19 @@ 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 "chef/tidy_server"
28
+ require "chef/tidy_common"
29
29
  end
30
30
 
31
31
  option :org_list,
32
- long: '--orgs ORG1,ORG2',
33
- description: 'Only apply to objects in the named organizations'
32
+ long: "--orgs ORG1,ORG2",
33
+ description: "Only apply to objects in the named organizations"
34
34
  end
35
35
  end
36
36
 
37
37
  def server
38
38
  @server ||= if Chef::Config.chef_server_root.nil?
39
- ui.warn('chef_server_root not found in knife configuration; using chef_server_url')
39
+ ui.warn("chef_server_root not found in knife configuration; using chef_server_url")
40
40
  Chef::TidyServer.from_chef_server_url(Chef::Config.chef_server_url)
41
41
  else
42
42
  Chef::TidyServer.new(Chef::Config.chef_server_root)
@@ -56,19 +56,19 @@ class Chef
56
56
  end
57
57
 
58
58
  def completion_message
59
- ui.stdout.puts ui.color('** Finished **', :magenta).to_s
59
+ ui.stdout.puts ui.color("** Finished **", :magenta).to_s
60
60
  end
61
61
 
62
62
  def action_needed_file_path
63
- ::File.expand_path('knife-tidy-actions-needed.txt')
63
+ ::File.expand_path("knife-tidy-actions-needed.txt")
64
64
  end
65
65
 
66
66
  def server_warnings_file_path
67
- ::File.expand_path('reports/knife-tidy-server-warnings.txt')
67
+ ::File.expand_path("reports/knife-tidy-server-warnings.txt")
68
68
  end
69
69
 
70
70
  def action_needed(msg, file_path = action_needed_file_path)
71
- ::File.open(file_path, 'a') do |f|
71
+ ::File.open(file_path, "a") do |f|
72
72
  f.write(msg + "\n")
73
73
  end
74
74
  end