facter 4.1.1 → 4.2.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/facter.rb +1 -1
  3. data/lib/facter/config.rb +2 -0
  4. data/lib/facter/custom_facts/core/execution/posix.rb +2 -2
  5. data/lib/facter/custom_facts/core/execution/windows.rb +1 -1
  6. data/lib/facter/custom_facts/util/directory_loader.rb +1 -1
  7. data/lib/facter/custom_facts/util/windows_root.rb +2 -1
  8. data/lib/facter/facts/freebsd/is_virtual.rb +1 -5
  9. data/lib/facter/facts/freebsd/virtual.rb +1 -2
  10. data/lib/facter/facts/linux/az_metadata.rb +1 -5
  11. data/lib/facter/facts/linux/cloud/provider.rb +1 -5
  12. data/lib/facter/facts/linux/ec2_metadata.rb +1 -5
  13. data/lib/facter/facts/linux/ec2_userdata.rb +1 -5
  14. data/lib/facter/facts/linux/hypervisors/xen.rb +1 -2
  15. data/lib/facter/facts/linux/is_virtual.rb +1 -5
  16. data/lib/facter/facts/linux/virtual.rb +1 -2
  17. data/lib/facter/facts/macosx/os/macosx/version.rb +6 -3
  18. data/lib/facter/facts/windows/az_metadata.rb +1 -1
  19. data/lib/facter/facts/windows/cloud/provider.rb +1 -1
  20. data/lib/facter/facts/windows/ec2_metadata.rb +1 -1
  21. data/lib/facter/facts/windows/ec2_userdata.rb +1 -1
  22. data/lib/facter/facts/windows/gce.rb +1 -1
  23. data/lib/facter/facts/windows/hypervisors/hyperv.rb +1 -1
  24. data/lib/facter/facts/windows/hypervisors/kvm.rb +2 -1
  25. data/lib/facter/facts/windows/hypervisors/virtualbox.rb +2 -2
  26. data/lib/facter/facts/windows/hypervisors/vmware.rb +1 -1
  27. data/lib/facter/facts/windows/hypervisors/xen.rb +3 -1
  28. data/lib/facter/facts/windows/is_virtual.rb +15 -0
  29. data/lib/facter/facts/windows/virtual.rb +15 -0
  30. data/lib/facter/framework/core/cache_manager.rb +2 -2
  31. data/lib/facter/framework/core/fact/external/external_fact_manager.rb +0 -1
  32. data/lib/facter/framework/core/fact/internal/internal_fact_manager.rb +41 -39
  33. data/lib/facter/framework/core/fact_filter.rb +4 -14
  34. data/lib/facter/framework/core/file_loader.rb +1 -1
  35. data/lib/facter/framework/parsers/query_parser.rb +5 -12
  36. data/lib/facter/models/fact_collection.rb +20 -3
  37. data/lib/facter/models/resolved_fact.rb +2 -3
  38. data/lib/facter/models/searched_fact.rb +2 -3
  39. data/lib/facter/resolvers/linux/networking.rb +18 -0
  40. data/lib/facter/resolvers/mountpoints.rb +16 -8
  41. data/lib/facter/resolvers/partitions.rb +1 -1
  42. data/lib/facter/resolvers/ruby.rb +1 -1
  43. data/lib/facter/resolvers/windows/ffi/identity_ffi.rb +5 -0
  44. data/lib/facter/resolvers/windows/identity.rb +1 -6
  45. data/lib/facter/resolvers/windows/virtualization.rb +46 -44
  46. data/lib/facter/resolvers/xen.rb +6 -1
  47. data/lib/facter/util/facts/posix/virtual_detector.rb +74 -0
  48. data/lib/facter/util/linux/if_inet6.rb +73 -0
  49. data/lib/facter/version.rb +1 -1
  50. metadata +26 -9
  51. data/lib/facter/facts/windows/virtualization/is_virtual.rb +0 -17
  52. data/lib/facter/facts/windows/virtualization/virtual.rb +0 -17
  53. data/lib/facter/framework/core/fact_augmenter.rb +0 -54
  54. data/lib/facter/util/facts/virtual_detector.rb +0 -83
@@ -1,21 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Facter
4
- # Filter inside value of a fact.
5
- # e.g. os.release.major is the user query, os.release is the fact
6
- # and major is the filter criteria inside tha fact
7
4
  class FactFilter
8
- def filter_facts!(searched_facts, user_query)
9
- filter_legacy_facts!(searched_facts) if user_query.empty?
10
- filter_blocked_legacy_facts!(searched_facts)
11
-
12
- searched_facts.each do |fact|
13
- fact.value = if fact.filter_tokens.any? && fact.value.respond_to?(:dig)
14
- fact.value.dig(*fact.filter_tokens)
15
- else
16
- fact.value
17
- end
18
- end
5
+ def filter_facts!(resolved_facts, user_query)
6
+ filter_legacy_facts!(resolved_facts) if user_query.empty?
7
+ filter_blocked_legacy_facts!(resolved_facts)
8
+ resolved_facts
19
9
  end
20
10
 
21
11
  private
@@ -37,6 +37,7 @@ load_dir(['config'])
37
37
  load_dir(['util'])
38
38
  load_dir(%w[util resolvers])
39
39
  load_dir(%w[util facts])
40
+ load_dir(%w[util facts posix])
40
41
  load_dir(%w[util resolvers networking])
41
42
 
42
43
  load_dir(['resolvers'])
@@ -58,5 +59,4 @@ os_hierarchy.each { |operating_system| load_dir(['resolvers', operating_system.d
58
59
  require 'facter/custom_facts/core/legacy_facter'
59
60
  load_dir(%w[framework utils])
60
61
 
61
- require 'facter/framework/core/fact_augmenter'
62
62
  require 'facter/framework/parsers/query_parser'
@@ -41,7 +41,7 @@ module Facter
41
41
  def no_user_query(loaded_facts)
42
42
  searched_facts = []
43
43
  loaded_facts.each do |loaded_fact|
44
- searched_facts << SearchedFact.new(loaded_fact.name, loaded_fact.klass, [], '', loaded_fact.type)
44
+ searched_facts << SearchedFact.new(loaded_fact.name, loaded_fact.klass, '', loaded_fact.type)
45
45
  end
46
46
  searched_facts
47
47
  end
@@ -59,7 +59,7 @@ module Facter
59
59
  return resolvable_fact_list if resolvable_fact_list.any?
60
60
  end
61
61
 
62
- resolvable_fact_list << SearchedFact.new(query, nil, [], query, :nil) if resolvable_fact_list.empty?
62
+ resolvable_fact_list << SearchedFact.new(query, nil, query, :nil) if resolvable_fact_list.empty?
63
63
 
64
64
  resolvable_fact_list
65
65
  end
@@ -73,7 +73,7 @@ module Facter
73
73
 
74
74
  next unless found_fact?(loaded_fact.name, query_fact)
75
75
 
76
- searched_fact = construct_loaded_fact(query_tokens, query_token_range, loaded_fact)
76
+ searched_fact = construct_loaded_fact(query_tokens, loaded_fact)
77
77
  resolvable_fact_list << searched_fact
78
78
  end
79
79
 
@@ -93,23 +93,16 @@ module Facter
93
93
  true
94
94
  end
95
95
 
96
- def construct_loaded_fact(query_tokens, query_token_range, loaded_fact)
97
- filter_tokens = construct_filter_tokens(query_tokens, query_token_range)
96
+ def construct_loaded_fact(query_tokens, loaded_fact)
98
97
  user_query = @query_list.any? ? query_tokens.join('.') : ''
99
98
  fact_name = loaded_fact.name.to_s
100
99
  klass_name = loaded_fact.klass
101
100
  type = loaded_fact.type
102
- sf = SearchedFact.new(fact_name, klass_name, filter_tokens, user_query, type)
101
+ sf = SearchedFact.new(fact_name, klass_name, user_query, type)
103
102
  sf.file = loaded_fact.file
104
103
 
105
104
  sf
106
105
  end
107
-
108
- def construct_filter_tokens(query_tokens, query_token_range)
109
- query_tokens.drop(query_token_range.size).map do |token|
110
- token =~ /^[0-9]+$/ ? token.to_i : token
111
- end
112
- end
113
106
  end
114
107
  end
115
108
  end
@@ -11,6 +11,12 @@ module Facter
11
11
  deep_to_h.to_yaml
12
12
  end
13
13
 
14
+ # Transorms a list of {Facter::ResolvedFact} into a nested collection.
15
+ # @param facts [Array<Facter::ResolvedFact>]
16
+ #
17
+ # @return [FactCollection]
18
+ #
19
+ # @api private
14
20
  def build_fact_collection!(facts)
15
21
  facts.each do |fact|
16
22
  next if %i[core legacy].include?(fact.type) && fact.value.nil?
@@ -23,15 +29,26 @@ module Facter
23
29
 
24
30
  def dig_fact(user_query)
25
31
  value(user_query)
26
- rescue KeyError
32
+ rescue KeyError, TypeError
27
33
  nil
28
34
  end
29
35
 
36
+ # Collection#fetch implementation for nested collections.
37
+ # @param user_query [String] the search terms, separated by "."
38
+ #
39
+ # @return [String]
40
+ #
41
+ # @example for fact_collection = { "os": { "name": "Darwin" } }
42
+ # fact_collection.fetch("os.name") #=> "Darwin"
43
+ #
44
+ # @api private
30
45
  def value(user_query)
31
46
  fetch(user_query) do
32
47
  split_user_query = Facter::Utils.split_user_query(user_query)
33
48
  split_user_query.reduce(self) do |memo, key|
34
- memo.fetch(key) { memo.fetch(key.to_s) } if memo.is_a?(Hash) || memo.is_a?(Array)
49
+ raise KeyError unless memo.respond_to?(:fetch)
50
+
51
+ memo.fetch(key) { memo.fetch(key.to_s) }
35
52
  end
36
53
  end
37
54
  end
@@ -60,7 +77,7 @@ module Facter
60
77
 
61
78
  def bury_fact(fact)
62
79
  split_fact_name = extract_fact_name(fact)
63
- bury(*split_fact_name + fact.filter_tokens << fact.value)
80
+ bury(*split_fact_name << fact.value)
64
81
  rescue NoMethodError
65
82
  @log.error("#{fact.type.to_s.capitalize} fact `#{fact.name}` cannot be added to collection."\
66
83
  ' The format of this fact is incompatible with other'\
@@ -3,14 +3,13 @@
3
3
  module Facter
4
4
  class ResolvedFact
5
5
  attr_reader :name, :type
6
- attr_accessor :user_query, :filter_tokens, :value, :file
6
+ attr_accessor :user_query, :value, :file
7
7
 
8
- def initialize(name, value = '', type = :core, user_query = nil, filter_tokens = [])
8
+ def initialize(name, value = '', type = :core, user_query = nil)
9
9
  @name = name
10
10
  @value = Utils.deep_stringify_keys(value)
11
11
  @type = type
12
12
  @user_query = user_query
13
- @filter_tokens = filter_tokens
14
13
  end
15
14
 
16
15
  def legacy?
@@ -2,13 +2,12 @@
2
2
 
3
3
  module Facter
4
4
  class SearchedFact
5
- attr_reader :name, :fact_class, :filter_tokens, :user_query, :type
5
+ attr_reader :name, :fact_class, :user_query, :type
6
6
  attr_accessor :file
7
7
 
8
- def initialize(fact_name, fact_class, filter_tokens, user_query, type)
8
+ def initialize(fact_name, fact_class, user_query, type)
9
9
  @name = fact_name
10
10
  @fact_class = fact_class
11
- @filter_tokens = filter_tokens
12
11
  @user_query = user_query
13
12
  @type = type
14
13
  end
@@ -20,6 +20,7 @@ module Facter
20
20
  add_info_from_routing_table
21
21
  retrieve_primary_interface
22
22
  Facter::Util::Resolvers::Networking.expand_main_bindings(@fact_list)
23
+ add_flags
23
24
  @fact_list[fact_name]
24
25
  end
25
26
 
@@ -72,6 +73,23 @@ module Facter
72
73
  compare_ips(routes6, :bindings6)
73
74
  end
74
75
 
76
+ def add_flags
77
+ flags = Facter::Util::Linux::IfInet6.read_flags
78
+ flags.each_pair do |iface, ips|
79
+ next unless @fact_list[:interfaces].key?(iface)
80
+
81
+ ips.each_pair do |ip, ip_flags|
82
+ next unless @fact_list[:interfaces][iface].key?(:bindings6)
83
+
84
+ @fact_list[:interfaces][iface][:bindings6].each do |binding|
85
+ next unless binding[:address] == ip
86
+
87
+ binding[:flags] = ip_flags
88
+ end
89
+ end
90
+ end
91
+ end
92
+
75
93
  def compare_ips(routes, binding_key)
76
94
  routes.each do |route|
77
95
  next unless @fact_list[:interfaces].key?(route[:interface])
@@ -67,10 +67,24 @@ module Facter
67
67
  end
68
68
 
69
69
  def get_mount_sizes(mount)
70
- stats = Facter::Util::Resolvers::FilesystemHelper.read_mountpoint_stats(mount[:path])
70
+ begin
71
+ stats = Facter::Util::Resolvers::FilesystemHelper.read_mountpoint_stats(mount[:path])
72
+ get_bytes_data(mount, stats)
73
+ rescue Sys::Filesystem::Error => e
74
+ @log.debug("Could not get stats for mountpoint #{mount[:path]}, got #{e}")
75
+ mount[:size_bytes] = mount[:available_bytes] = mount[:used_bytes] = 0
76
+ end
77
+
78
+ populate_mount(mount)
79
+ end
71
80
 
72
- get_bytes_data(mount, stats)
81
+ def get_bytes_data(mount, stats)
82
+ mount[:size_bytes] = stats.bytes_total.abs
83
+ mount[:available_bytes] = stats.bytes_available.abs
84
+ mount[:used_bytes] = stats.bytes_used.abs
85
+ end
73
86
 
87
+ def populate_mount(mount)
74
88
  total_bytes = mount[:used_bytes] + mount[:available_bytes]
75
89
  mount[:capacity] = Facter::Util::Resolvers::FilesystemHelper.compute_capacity(mount[:used_bytes], total_bytes)
76
90
 
@@ -78,12 +92,6 @@ module Facter
78
92
  mount[:available] = Facter::Util::Facts::UnitConverter.bytes_to_human_readable(mount[:available_bytes])
79
93
  mount[:used] = Facter::Util::Facts::UnitConverter.bytes_to_human_readable(mount[:used_bytes])
80
94
  end
81
-
82
- def get_bytes_data(mount, stats)
83
- mount[:size_bytes] = stats.bytes_total.abs
84
- mount[:available_bytes] = stats.bytes_available.abs
85
- mount[:used_bytes] = stats.bytes_used.abs
86
- end
87
95
  end
88
96
  end
89
97
  end
@@ -109,7 +109,7 @@ module Facter
109
109
 
110
110
  output = Facter::Core::Execution.execute("which #{command}", logger: log)
111
111
 
112
- blkid_and_lsblk[:command_exists_key] = !output.empty?
112
+ blkid_and_lsblk[command_exists_key] = !output.empty?
113
113
  end
114
114
 
115
115
  def execute_and_extract_blkid_info
@@ -13,7 +13,7 @@ module Facter
13
13
  end
14
14
 
15
15
  def retrieve_ruby_information(fact_name)
16
- @fact_list[:sitedir] = RbConfig::CONFIG['sitelibdir']
16
+ @fact_list[:sitedir] = RbConfig::CONFIG['sitelibdir'] if RbConfig::CONFIG['sitedir']
17
17
  @fact_list[:platform] = RUBY_PLATFORM
18
18
  @fact_list[:version] = RUBY_VERSION
19
19
  @fact_list[fact_name]
@@ -12,4 +12,9 @@ module IdentityFFI
12
12
  ffi_convention :stdcall
13
13
  ffi_lib :shell32
14
14
  attach_function :IsUserAnAdmin, [], :win32_bool
15
+
16
+ def self.privileged?
17
+ result = self.IsUserAnAdmin()
18
+ result && result != FFI::WIN32FALSE
19
+ end
15
20
  end
@@ -31,12 +31,7 @@ module Facter
31
31
  return
32
32
  end
33
33
 
34
- { user: name_ptr.read_wide_string_with_length(size_ptr.read_uint32), privileged: privileged? }
35
- end
36
-
37
- def privileged?
38
- result = IdentityFFI::IsUserAnAdmin()
39
- result && result != FFI::WIN32FALSE
34
+ { user: name_ptr.read_wide_string_with_length(size_ptr.read_uint32), privileged: IdentityFFI.privileged? }
40
35
  end
41
36
 
42
37
  def retrieve_facts(fact_name)
@@ -2,63 +2,65 @@
2
2
 
3
3
  module Facter
4
4
  module Resolvers
5
- class Virtualization < BaseResolver
6
- @log = Facter::Log.new(self)
5
+ module Windows
6
+ class Virtualization < BaseResolver
7
+ @log = Facter::Log.new(self)
7
8
 
8
- init_resolver
9
+ init_resolver
9
10
 
10
- class << self
11
- # Virtual
12
- # Is_Virtual
11
+ class << self
12
+ # Virtual
13
+ # Is_Virtual
13
14
 
14
- MODEL_HASH = { 'VirtualBox' => 'virtualbox', 'VMware' => 'vmware', 'KVM' => 'kvm',
15
- 'Bochs' => 'bochs', 'Google' => 'gce', 'OpenStack' => 'openstack' }.freeze
15
+ MODEL_HASH = { 'VirtualBox' => 'virtualbox', 'VMware' => 'vmware', 'KVM' => 'kvm',
16
+ 'Bochs' => 'bochs', 'Google' => 'gce', 'OpenStack' => 'openstack' }.freeze
16
17
 
17
- private
18
+ private
18
19
 
19
- def post_resolve(fact_name, _options)
20
- @fact_list.fetch(fact_name) { read_fact_from_computer_system(fact_name) }
21
- end
22
-
23
- def read_fact_from_computer_system(fact_name)
24
- win = Facter::Util::Windows::Win32Ole.new
25
- comp = win.exec_query('SELECT Manufacturer,Model,OEMStringArray FROM Win32_ComputerSystem')
26
- unless comp
27
- @log.debug 'WMI query returned no results for Win32_ComputerSystem with values'\
28
- ' Manufacturer, Model and OEMStringArray.'
29
- return
20
+ def post_resolve(fact_name, _options)
21
+ @fact_list.fetch(fact_name) { read_fact_from_computer_system(fact_name) }
30
22
  end
31
23
 
32
- build_fact_list(comp)
33
- @fact_list[fact_name]
34
- end
24
+ def read_fact_from_computer_system(fact_name)
25
+ win = Facter::Util::Windows::Win32Ole.new
26
+ comp = win.exec_query('SELECT Manufacturer,Model,OEMStringArray FROM Win32_ComputerSystem')
27
+ unless comp
28
+ @log.debug 'WMI query returned no results for Win32_ComputerSystem with values'\
29
+ ' Manufacturer, Model and OEMStringArray.'
30
+ return
31
+ end
35
32
 
36
- def determine_hypervisor_by_model(comp)
37
- MODEL_HASH[MODEL_HASH.keys.find { |key| comp.Model =~ /^#{key}/ }]
38
- end
33
+ build_fact_list(comp)
34
+ @fact_list[fact_name]
35
+ end
39
36
 
40
- def determine_hypervisor_by_manufacturer(comp)
41
- manufacturer = comp.Manufacturer
42
- if comp.Model =~ /^Virtual Machine/ && manufacturer =~ /^Microsoft/
43
- 'hyperv'
44
- elsif manufacturer =~ /^Xen/
45
- 'xen'
46
- elsif manufacturer =~ /^Amazon EC2/
47
- 'kvm'
48
- else
49
- 'physical'
37
+ def determine_hypervisor_by_model(comp)
38
+ MODEL_HASH[MODEL_HASH.keys.find { |key| comp.Model =~ /^#{key}/ }]
39
+ end
40
+
41
+ def determine_hypervisor_by_manufacturer(comp)
42
+ manufacturer = comp.Manufacturer
43
+ if comp.Model =~ /^Virtual Machine/ && manufacturer =~ /^Microsoft/
44
+ 'hyperv'
45
+ elsif manufacturer =~ /^Xen/
46
+ 'xen'
47
+ elsif manufacturer =~ /^Amazon EC2/
48
+ 'kvm'
49
+ else
50
+ 'physical'
51
+ end
50
52
  end
51
- end
52
53
 
53
- def build_fact_list(comp)
54
- @fact_list[:oem_strings] = []
55
- @fact_list[:oem_strings] += comp.to_enum.map(&:OEMStringArray).flatten
54
+ def build_fact_list(comp)
55
+ @fact_list[:oem_strings] = []
56
+ @fact_list[:oem_strings] += comp.to_enum.map(&:OEMStringArray).flatten
56
57
 
57
- comp = comp.to_enum.first
58
- hypervisor = determine_hypervisor_by_model(comp) || determine_hypervisor_by_manufacturer(comp)
58
+ comp = comp.to_enum.first
59
+ hypervisor = determine_hypervisor_by_model(comp) || determine_hypervisor_by_manufacturer(comp)
59
60
 
60
- @fact_list[:virtual] = hypervisor
61
- @fact_list[:is_virtual] = hypervisor.include?('physical') ? false : true
61
+ @fact_list[:virtual] = hypervisor
62
+ @fact_list[:is_virtual] = hypervisor.include?('physical') ? false : true
63
+ end
62
64
  end
63
65
  end
64
66
  end
@@ -56,7 +56,12 @@ module Facter
56
56
  end
57
57
 
58
58
  def find_command
59
- return XEN_TOOLSTACK if File.exist?(XEN_TOOLSTACK)
59
+ num_stacks = 0
60
+ XEN_COMMANDS.each do |command|
61
+ num_stacks += 1 if File.exist?(command)
62
+ end
63
+
64
+ return XEN_TOOLSTACK if num_stacks > 1 && File.exist?(XEN_TOOLSTACK)
60
65
 
61
66
  XEN_COMMANDS.each { |command| return command if File.exist?(command) }
62
67
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Facter
4
+ module Util
5
+ module Facts
6
+ module Posix
7
+ module VirtualDetector
8
+ class << self
9
+ def platform
10
+ @@fact_value ||= check_docker_lxc || check_freebsd || check_gce || retrieve_from_virt_what
11
+ @@fact_value ||= check_vmware || check_open_vz || check_vserver || check_xen || check_other_facts
12
+ @@fact_value ||= check_lspci || 'physical'
13
+
14
+ @@fact_value
15
+ end
16
+
17
+ private
18
+
19
+ def check_docker_lxc
20
+ Facter::Resolvers::Containers.resolve(:vm)
21
+ end
22
+
23
+ def check_gce
24
+ bios_vendor = Facter::Resolvers::Linux::DmiBios.resolve(:bios_vendor)
25
+ 'gce' if bios_vendor&.include?('Google')
26
+ end
27
+
28
+ def check_vmware
29
+ Facter::Resolvers::Vmware.resolve(:vm)
30
+ end
31
+
32
+ def retrieve_from_virt_what
33
+ Facter::Resolvers::VirtWhat.resolve(:vm)
34
+ end
35
+
36
+ def check_open_vz
37
+ Facter::Resolvers::OpenVz.resolve(:vm)
38
+ end
39
+
40
+ def check_vserver
41
+ Facter::Resolvers::VirtWhat.resolve(:vserver)
42
+ end
43
+
44
+ def check_xen
45
+ Facter::Resolvers::Xen.resolve(:vm)
46
+ end
47
+
48
+ def check_freebsd
49
+ return unless Object.const_defined?('Facter::Resolvers::Freebsd::Virtual')
50
+
51
+ Facter::Resolvers::Freebsd::Virtual.resolve(:vm)
52
+ end
53
+
54
+ def check_other_facts
55
+ bios_vendor = Facter::Resolvers::Linux::DmiBios.resolve(:bios_vendor)
56
+ return 'kvm' if bios_vendor&.include?('Amazon EC2')
57
+
58
+ product_name = Facter::Resolvers::Linux::DmiBios.resolve(:product_name)
59
+ return unless product_name
60
+
61
+ Facter::Util::Facts::HYPERVISORS_HASH.each { |key, value| return value if product_name.include?(key) }
62
+
63
+ nil
64
+ end
65
+
66
+ def check_lspci
67
+ Facter::Resolvers::Lspci.resolve(:vm)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end