foreman_monitoring 0.0.3 → 0.1.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -1
  3. data/app/controllers/api/v2/monitoring_results_controller.rb +8 -0
  4. data/app/controllers/concerns/foreman_monitoring/hosts_controller_extensions.rb +140 -0
  5. data/app/helpers/concerns/foreman_monitoring/hosts_helper_ext.rb +35 -11
  6. data/app/lib/proxy_api/monitoring.rb +35 -0
  7. data/app/models/concerns/foreman_monitoring/host_extensions.rb +48 -8
  8. data/app/models/concerns/foreman_monitoring/hostgroup_extensions.rb +15 -0
  9. data/app/models/concerns/orchestration/monitoring.rb +105 -0
  10. data/app/models/host_status/monitoring_status.rb +5 -1
  11. data/app/models/setting/monitoring.rb +23 -6
  12. data/app/overrides/add_host_multiple_power_set_downtime_checkbox.rb +5 -0
  13. data/app/overrides/add_host_set_downtime_modal.rb +5 -0
  14. data/app/services/monitoring.rb +56 -3
  15. data/app/views/hosts/_downtime_fields.html.erb +3 -0
  16. data/app/views/hosts/_host_downtime_checkbox.html.erb +8 -0
  17. data/app/views/hosts/_set_host_downtime.html.erb +20 -0
  18. data/app/views/hosts/select_multiple_downtime.html.erb +5 -0
  19. data/app/views/hosts/select_multiple_monitoring_proxy.html.erb +5 -0
  20. data/app/views/monitoring_results/_host_tab.html.erb +2 -0
  21. data/app/views/monitoring_results/_host_tab_pane.html.erb +2 -0
  22. data/config/routes.rb +16 -0
  23. data/db/migrate/20161220201510_add_monitoring_proxy_id_to_host_and_hostgroup.rb +11 -0
  24. data/lib/foreman_monitoring/engine.rb +35 -3
  25. data/lib/foreman_monitoring/version.rb +1 -1
  26. data/test/factories/host.rb +6 -0
  27. data/test/functional/hosts_controller_test.rb +190 -0
  28. data/test/lib/proxy_api/monitoring_test.rb +30 -0
  29. data/test/test_plugin_helper.rb +4 -0
  30. data/test/unit/host_status/monitoring_status_test.rb +7 -2
  31. data/test/unit/host_test.rb +111 -5
  32. data/test/unit/monitoring_test.rb +9 -3
  33. metadata +19 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 64c6df30e7204048fc85d7ce4e6de17ba28d14c5
4
- data.tar.gz: 3e14672d74d600a9e05aff1a92846b804c07aa0b
3
+ metadata.gz: 3d9186b9fa48b695bea2617e3c589b22d0df22bd
4
+ data.tar.gz: f3dd5db766d37990e59d09ffdf9881a830abc134
5
5
  SHA512:
6
- metadata.gz: 8e37b4565ae23bc25f9319ce81737751eb95cf1ebe11840aad9499cbacf37445447eb784d6c95a8022702effc2cf751748ee109b41f621960daf8a245d88eb52
7
- data.tar.gz: ceb07dd17ccef4baf8311faa60afbfb8ad03a73c2b842eb7fb9dcfeb1865de4feebb9448292cc1e5c46f43f76db08c877a0484c56d63b53ee4d20e3c6c6e7b7a
6
+ metadata.gz: 25700b1a96cebf6cfb1e829b54dfef9f6fd6f934ffbbd1e8a9fa753ee9e51f0a406c478c0bdba8e8cd2b46bbeaf5ae3d2be0a06da9524ea77740b8e042e32f61
7
+ data.tar.gz: 1f714e3081f6adeb50b7f01f30c6855dc237f3613210993719fc0f0d97ed34828190f58c784e9ac93a6afb6c54d2049c1bd5b82f7e76d088553b3af7ae8f1479
data/README.md CHANGED
@@ -1,14 +1,41 @@
1
1
  # Foreman Monitoring Plugin
2
2
 
3
3
  This is a Foreman plugin for monitoring system integration.
4
+ It allows to manage hosts and downtimes and to display status
5
+ information from the monitoring solution.
6
+
7
+ It requires the Smart Proxy plugin [monitoring](https://github.com/theforeman/smart_proxy_monitoring)
8
+ for communication with the monitoring system. See its documentation
9
+ for supported monitoring solutions and detailed configuration instructions.
4
10
 
5
11
  # Installation
6
12
 
7
13
  See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
8
- for how to install Foreman plugins
14
+ for how to install Foreman plugins.
15
+
16
+ The gem name is `foreman_monitoring`.
17
+
18
+ RPM users can install the `tfm-rubygem-foreman_monitoring` package.
19
+
20
+ This plug-in has not been packaged for Debian, yet.
9
21
 
10
22
  # Usage
11
23
 
24
+ For managing a host in the monitoring solution a Smart Proxy providing
25
+ the `monitoring` feature has to be assigned. This can be done during
26
+ provisioning or as a bulk action from the host overview.
27
+
28
+ You can configure the default action which will be done during host
29
+ provisioning and deprovisioning. Provisioning allows to create a monitoring
30
+ object or take no action while deprovisioing allows to delete the monitoring
31
+ object, set a downtime or take no action. For rebuild it will by default
32
+ set a downtime.
33
+
34
+ The plugin will show you the monitoring status as a sub-status and a detail
35
+ panel. You can configure if the sub-status should affect the global status.
36
+
37
+ Furthermore it allows to individually set a downtime at the host detail view
38
+ or as a bulk action from the host overview.
12
39
 
13
40
  ## Contributing
14
41
 
@@ -6,6 +6,14 @@ module Api
6
6
 
7
7
  add_smart_proxy_filters :create, :features => 'Monitoring'
8
8
 
9
+ api :POST, '/monitoring_results', N_('Import monitoring result')
10
+ param :host, String, :desc => N_('FQDN of the host that the results are for'), :required => true
11
+ param :service, String, :desc => N_('Name of the service the results belong to'), :required => true
12
+ param :timestamp, String, :desc => N_('Timestamp of the results')
13
+ param :acknowledged, [true, false], :desc => N_('Is the result acknowledged?')
14
+ param :downtime, [true, false], :desc => N_('Is the result in downtime?')
15
+ param :result, [0, 1, 2, 3], :desc => N_('State of the monitoring result (0 -> ok, 1 -> warning, 2 -> critical, 3 -> unknown)')
16
+
9
17
  def create
10
18
  begin
11
19
  MonitoringResult.import(monitoring_result_params.with_indifferent_access)
@@ -0,0 +1,140 @@
1
+ module ForemanMonitoring
2
+ module HostsControllerExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :find_resource_with_monitoring, :only => [:downtime]
7
+ before_action :find_multiple_with_monitoring, :only => [:select_multiple_downtime, :update_multiple_downtime,
8
+ :select_multiple_monitoring_proxy, :update_multiple_monitoring_proxy]
9
+ before_action :validate_host_downtime_params, :only => [:downtime]
10
+ before_action :validate_hosts_downtime_params, :only => [:update_multiple_downtime]
11
+ before_action :validate_multiple_monitoring_proxy, :only => :update_multiple_monitoring_proxy
12
+
13
+ alias_method :find_resource_with_monitoring, :find_resource
14
+ alias_method :find_multiple_with_monitoring, :find_multiple
15
+ alias_method_chain :update_multiple_power_state, :monitoring
16
+ end
17
+
18
+ def downtime
19
+ unless @host.downtime_host(downtime_options)
20
+ process_error(:redirect => host_path, :error_msg => @host.errors.full_messages.to_sentence)
21
+ return false
22
+ end
23
+ process_success :success_msg => _('Created downtime for %s') % (@host), :success_redirect => :back
24
+ end
25
+
26
+ def select_multiple_downtime
27
+ end
28
+
29
+ def update_multiple_downtime
30
+ failed_hosts = {}
31
+
32
+ @hosts.each do |host|
33
+ unless host.monitored?
34
+ failed_hosts[host.name] = _('is not monitored')
35
+ next
36
+ end
37
+ begin
38
+ unless host.downtime_host(downtime_options)
39
+ error_message = host.errors.full_messages.to_sentence
40
+ failed_hosts[host.name] = error_message
41
+ logger.error "Failed to set a host downtime for #{host}: #{error_message}"
42
+ end
43
+ rescue => error
44
+ failed_hosts[host.name] = error
45
+ Foreman::Logging.exception(_('Failed to set a host downtime for %s.') % host, error)
46
+ end
47
+ end
48
+
49
+ if failed_hosts.empty?
50
+ notice _('A downtime was set for the selected hosts.')
51
+ else
52
+ error n_('A downtime clould not be set for host: %s.',
53
+ 'A downtime could not be set for hosts: %s.',
54
+ failed_hosts.count) % failed_hosts.map { |h, err| "#{h} (#{err})" }.to_sentence
55
+ end
56
+ redirect_back_or_to hosts_path
57
+ end
58
+
59
+ def validate_multiple_monitoring_proxy
60
+ validate_multiple_proxy(select_multiple_monitoring_proxy_hosts_path)
61
+ end
62
+
63
+ def select_multiple_monitoring_proxy
64
+ end
65
+
66
+ def update_multiple_monitoring_proxy
67
+ update_multiple_proxy(_('Monitoring'), :monitoring_proxy=)
68
+ end
69
+
70
+ def update_multiple_power_state_with_monitoring
71
+ options = {
72
+ :comment => 'Power state changed in Foreman',
73
+ :author => "Foreman User #{User.current}",
74
+ :start_time => DateTime.now.to_time.to_i,
75
+ :end_time => DateTime.now.advance(:minutes => 30).to_time.to_i
76
+ }
77
+ if User.current.allowed_to?(:controller => :hosts, :action => :select_multiple_downtime) && params[:power][:set_downtime]
78
+ @hosts.each do |host|
79
+ unless host.monitored?
80
+ logger.debug "Not setting a downtime for #{host} as it is not monitored."
81
+ next
82
+ end
83
+ if host.downtime_host(options)
84
+ logger.debug "Set a host downtime for #{host}."
85
+ else
86
+ logger.error "Failed to set a host downtime for #{host}: #{host.errors.full_messages.to_sentence}"
87
+ end
88
+ end
89
+ end
90
+ update_multiple_power_state_without_monitoring
91
+ end
92
+
93
+ private
94
+
95
+ def downtime_options
96
+ {
97
+ :comment => params[:downtime][:comment],
98
+ :author => "Foreman User #{User.current}",
99
+ :start_time => DateTime.parse(params[:downtime][:starttime]).to_time.to_i,
100
+ :end_time => DateTime.parse(params[:downtime][:endtime]).to_time.to_i
101
+ }
102
+ end
103
+
104
+ def validate_host_downtime_params
105
+ validate_downtime_params(host_path)
106
+ end
107
+
108
+ def validate_hosts_downtime_params
109
+ validate_downtime_params(hosts_path)
110
+ end
111
+
112
+ def validate_downtime_params(redirect_url)
113
+ if params[:downtime].blank? || (params[:downtime][:comment]).blank?
114
+ process_error(:redirect => redirect_url, :error_msg => 'No comment for downtime set!')
115
+ return false
116
+ end
117
+ if (params[:downtime][:starttime]).blank? || (params[:downtime][:endtime]).blank?
118
+ process_error(:redirect => redirect_url, :error_msg => 'No start/endtime for downtime!')
119
+ return false
120
+ end
121
+ begin
122
+ DateTime.parse(params[:downtime][:starttime])
123
+ DateTime.parse(params[:downtime][:endtime])
124
+ rescue ArgumentError
125
+ process_error(:redirect => redirect_url, :error_msg => 'Invalid start/endtime for downtime!')
126
+ return false
127
+ end
128
+ end
129
+
130
+ def action_permission
131
+ case params[:action]
132
+ when 'downtime', 'select_multiple_downtime', 'update_multiple_downtime',
133
+ 'select_multiple_monitoring_proxy', 'update_multiple_monitoring_proxy'
134
+ :downtime
135
+ else
136
+ super
137
+ end
138
+ end
139
+ end
140
+ end
@@ -2,18 +2,35 @@ module ForemanMonitoring
2
2
  module HostsHelperExt
3
3
  extend ActiveSupport::Concern
4
4
 
5
- # included do
6
- # alias_method_chain :host_title_actions, :monitoring
7
- # end
5
+ included do
6
+ alias_method_chain :host_title_actions, :monitoring
7
+ alias_method_chain :multiple_actions, :monitoring
8
+ end
9
+
10
+ def multiple_actions_with_monitoring
11
+ return multiple_actions_without_monitoring unless authorized_for(:controller => :hosts, :action => :select_multiple_downtime)
12
+ multiple_actions_without_monitoring + [[_('Set downtime'), select_multiple_downtime_hosts_path], [_('Change Monitoring Proxy'), select_multiple_monitoring_proxy_hosts_path]]
13
+ end
8
14
 
9
- # def host_title_actions_with_monitoring(host)
10
- # title_actions(
11
- # button_group(
12
- # link_to(_('Monitoring'), monitoring_show_host_path(host), :target => '_blank', :class => 'btn btn-default')
13
- # )
14
- # )
15
- # host_title_actions_without_monitoring(host)
16
- # end
15
+ def host_title_actions_with_monitoring(host)
16
+ title_actions(
17
+ button_group(
18
+ display_link_if_authorized(_('Downtime'),
19
+ hash_for_host_path(:id => host).merge(:auth_object => host,
20
+ :permission => :manage_host_downtimes,
21
+ :anchor => 'set_host_downtime'),
22
+ :class => 'btn btn-default',
23
+ :disabled => !host.monitored?,
24
+ :title => _('Set a downtime for this host'),
25
+ :id => 'host-downtime',
26
+ :data => { :toggle => 'modal',
27
+ :target => '#set_host_downtime'
28
+ }
29
+ )
30
+ )
31
+ )
32
+ host_title_actions_without_monitoring(host)
33
+ end
17
34
 
18
35
  def host_monitoring_result_icon_class(result)
19
36
  icon_class = case result
@@ -42,5 +59,12 @@ module ForemanMonitoring
42
59
  'status-question'
43
60
  end
44
61
  end
62
+
63
+ def datetime_f(f, attr, options = {})
64
+ field(f, attr, options) do
65
+ addClass options, 'form-control'
66
+ f.datetime_local_field attr, options
67
+ end
68
+ end
45
69
  end
46
70
  end
@@ -10,5 +10,40 @@ module ProxyAPI
10
10
  rescue => e
11
11
  raise ProxyException.new(url, e, N_('Unable to set downtime for %s') % host)
12
12
  end
13
+
14
+ def remove_host_downtime(host, args = {})
15
+ parse(delete("downtime/host/#{host}?#{args.to_query}"))
16
+ rescue => e
17
+ raise ProxyException.new(url, e, N_('Unable to remove downtime for %s') % host)
18
+ end
19
+
20
+ def create_host(host, attributes = {})
21
+ parse(put({:attributes => attributes}, "host/#{host}"))
22
+ rescue => e
23
+ raise ProxyException.new(url, e, N_('Unable to create monitoring host object for %s') % host)
24
+ end
25
+
26
+ def update_host(host, attributes = {})
27
+ parse(post({:attributes => attributes}, "host/#{host}"))
28
+ rescue => e
29
+ raise ProxyException.new(url, e, N_('Unable to update monitoring host object for %s') % host)
30
+ end
31
+
32
+ def delete_host(host)
33
+ raise Foreman::Exception.new('Missing hostname.') if host.blank?
34
+ parse(delete("host/#{host}"))
35
+ rescue RestClient::ResourceNotFound
36
+ true
37
+ rescue => e
38
+ raise ProxyException.new(url, e, N_('Unable to delete monitoring host object for %s') % host)
39
+ end
40
+
41
+ def query_host(host)
42
+ parse(get("host/#{host}"))
43
+ rescue RestClient::ResourceNotFound
44
+ nil
45
+ rescue => e
46
+ raise ProxyException.new(url, e, N_('Unable to query monitoring host object for %s') % host)
47
+ end
13
48
  end
14
49
  end
@@ -2,10 +2,14 @@ module ForemanMonitoring
2
2
  module HostExtensions
3
3
  extend ActiveSupport::Concern
4
4
  included do
5
- before_destroy :downtime_host_destroy
5
+ include Orchestration::Monitoring
6
+
6
7
  after_build :downtime_host_build
7
8
 
8
- has_many :monitoring_results, :foreign_key => 'host_id'
9
+ alias_method_chain :smart_proxy_ids, :monitoring_proxy
10
+ alias_method_chain :hostgroup_inherited_attributes, :monitoring
11
+
12
+ has_many :monitoring_results, :dependent => :destroy, :foreign_key => 'host_id'
9
13
  end
10
14
 
11
15
  def monitoring_status(options = {})
@@ -24,19 +28,55 @@ module ForemanMonitoring
24
28
  downtime_host(:comment => _('Host rebuilt in Foreman'))
25
29
  end
26
30
 
27
- def downtime_host_destroy
28
- downtime_host(:comment => _('Host deleted in Foreman'))
29
- end
30
-
31
31
  def downtime_host(options)
32
- return unless monitoring_results.any?
32
+ return unless monitored?
33
33
  begin
34
- monitoring = Monitoring.new
35
34
  monitoring.set_downtime_host(self, options)
36
35
  rescue ProxyAPI::ProxyException => e
37
36
  errors.add(:base, _("Error setting downtime: '%s'") % e.message)
38
37
  end
39
38
  errors.empty?
40
39
  end
40
+
41
+ def monitored?
42
+ monitoring_proxy.present?
43
+ end
44
+
45
+ def hostgroup_inherited_attributes_with_monitoring
46
+ hostgroup_inherited_attributes_without_monitoring + ['monitoring_proxy_id']
47
+ end
48
+
49
+ def smart_proxy_ids_with_monitoring_proxy
50
+ ids = smart_proxy_ids_without_monitoring_proxy
51
+ [monitoring_proxy, hostgroup.try(:monitoring_proxy)].compact.each do |proxy|
52
+ ids << proxy.id
53
+ end
54
+ ids
55
+ end
56
+
57
+ def monitoring_attributes
58
+ {
59
+ :ip => ip,
60
+ :ip6 => ip6,
61
+ :architecture => architecture.try(:name),
62
+ :os => operatingsystem.try(:to_label),
63
+ :osfamily => operatingsystem.try(:family),
64
+ :virtual => provider != 'BareMetal',
65
+ :provider => provider,
66
+ :compute_resource => compute_resource.try(:to_label),
67
+ :hostgroup => hostgroup.try(:to_label),
68
+ :organization => organization.try(:name),
69
+ :location => organization.try(:name),
70
+ :comment => comment,
71
+ :environment => environment.try(:to_s),
72
+ :owner_name => owner.try(:name)
73
+ }
74
+ end
75
+
76
+ private
77
+
78
+ def monitoring
79
+ Monitoring.new(:monitoring_proxy => monitoring_proxy)
80
+ end
41
81
  end
42
82
  end
@@ -0,0 +1,15 @@
1
+ module ForemanMonitoring
2
+ module HostgroupExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ def monitoring_proxy
6
+ return super unless ancestry.present?
7
+ SmartProxy.find_by_id(inherited_monitoring_proxy_id)
8
+ end
9
+
10
+ def inherited_monitoring_proxy_id
11
+ return monitoring_proxy_id unless ancestry.present?
12
+ self[:monitoring_proxy_id] || self.class.sort_by_ancestry(ancestors.where('monitoring_proxy_id is not NULL')).last.try(:monitoring_proxy_id)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,105 @@
1
+ module Orchestration::Monitoring
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ after_validation :queue_monitoring
6
+ before_destroy :queue_monitoring_destroy
7
+ end
8
+
9
+ protected
10
+
11
+ def queue_monitoring
12
+ return unless monitored? && errors.empty?
13
+ clear_monitoring_object
14
+ !monitoring_object.has_key?(:attrs) ? queue_monitoring_create : queue_monitoring_update
15
+ end
16
+
17
+ def queue_monitoring_create
18
+ queue.create(:name => _("Create monitoring object for %s") % self, :priority => 20,
19
+ :action => [self, :setMonitoring]) if ::Monitoring.create_action?(:create)
20
+ end
21
+
22
+ def queue_monitoring_update
23
+ return unless monitoring_update_required?(monitoring_object[:attrs], monitoring_attributes)
24
+ Rails.logger.debug("Detected a change to the monitoring object is required.")
25
+ queue.create(:name => _("Monitoring update for %s") % old, :priority => 2,
26
+ :action => [self, :setMonitoringUpdate]) if ::Monitoring.create_action?(:create)
27
+ end
28
+
29
+ def queue_monitoring_destroy
30
+ return unless monitored? && errors.empty?
31
+ queue.create(:name => _("Removing monitoring object for %s") % self, :priority => 2,
32
+ :action => [self, :delMonitoring]) if ::Monitoring.delete_action?(:delete)
33
+ queue.create(:name => _("Set monitoring downtime for %s") % self, :priority => 2,
34
+ :action => [self, :setMonitoringDowntime]) if ::Monitoring.delete_action?(:downtime)
35
+ end
36
+
37
+ def setMonitoring
38
+ Rails.logger.info "Adding Monitoring object for #{name}"
39
+ monitoring.create_host(self)
40
+ rescue => e
41
+ failure _("Failed to create a monitoring object %{name}: %{message}\n ") % { :name => name, :message => e.message }, e
42
+ end
43
+
44
+ def delMonitoring
45
+ Rails.logger.info "Deleting Monitoring object for #{name}"
46
+ monitoring.delete_host(self)
47
+ rescue => e
48
+ failure _("Failed to delete a monitoring object %{name}: %{message}\n ") % { :name => name, :message => e.message }, e
49
+ end
50
+
51
+ def setMonitoringUpdate
52
+ Rails.logger.info "Updating Monitoring object for #{name}"
53
+ monitoring.update_host(self)
54
+ rescue => e
55
+ failure _("Failed to update a monitoring object %{name}: %{message}\n ") % { :name => name, :message => e.message }, e
56
+ end
57
+
58
+ def delMonitoringUpdate; end
59
+
60
+ def setMonitoringDowntime
61
+ Rails.logger.info "Setting Monitoring downtime for #{name}"
62
+ monitoring.set_downtime_host(self, monitoring_downtime_defaults)
63
+ rescue => e
64
+ failure _("Failed to set a monitoring downtime for %{name}: %{message}\n ") % { :name => name, :message => e.message }, e
65
+ end
66
+
67
+ def delMonitoringDowntime
68
+ Rails.logger.info "Deleting Monitoring downtime for #{name}"
69
+ monitoring.del_downtime_host(self, monitoring_downtime_defaults)
70
+ rescue => e
71
+ failure _("Failed to set a monitoring downtime for %{name}: %{message}\n ") % { :name => name, :message => e.message }, e
72
+ end
73
+
74
+ def monitoring_object
75
+ @monitoring_object || monitoring.query_host(self)
76
+ end
77
+
78
+ def clear_monitoring_object
79
+ @monitoring_object = nil
80
+ true
81
+ end
82
+
83
+ private
84
+
85
+ def monitoring_downtime_defaults
86
+ {
87
+ :comment => _('Host deleted in Foreman')
88
+ }
89
+ end
90
+
91
+ def monitoring_update_required?(actual_attrs, desired_attrs)
92
+ return true if actual_attrs.deep_symbolize_keys.keys != desired_attrs.deep_symbolize_keys.keys
93
+ actual_attrs.deep_symbolize_keys.merge(desired_attrs.deep_symbolize_keys) do |k, actual_v, desired_v|
94
+ if actual_v.is_a?(Hash) && desired_v.is_a?(Hash)
95
+ return true if monitoring_update_required?(actual_v, desired_v)
96
+ elsif actual_v.to_s != desired_v.to_s
97
+ Rails.logger.debug "Scheduling monitoring host object update because #{k} changed it's value from '#{actual_v}' (#{actual_v.class}) to '#{desired_v}' (#{desired_v.class})"
98
+ return true
99
+ end
100
+ desired_v
101
+ end
102
+ Rails.logger.debug "No monitoring update required."
103
+ false
104
+ end
105
+ end