foreman_monitoring 0.1.1 → 2.0.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 (44) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +14 -7
  3. data/Rakefile +0 -0
  4. data/app/controllers/api/v2/downtime_controller.rb +63 -0
  5. data/app/controllers/api/v2/monitoring_results_controller.rb +5 -4
  6. data/app/controllers/concerns/foreman_monitoring/find_host_by_client_cert.rb +59 -0
  7. data/app/controllers/concerns/foreman_monitoring/hosts_controller_extensions.rb +7 -5
  8. data/app/helpers/concerns/foreman_monitoring/hosts_helper_ext.rb +11 -13
  9. data/app/lib/proxy_api/monitoring.rb +9 -6
  10. data/app/models/concerns/foreman_monitoring/host_extensions.rb +6 -3
  11. data/app/models/concerns/foreman_monitoring/hostgroup_extensions.rb +7 -3
  12. data/app/models/concerns/orchestration/monitoring.rb +22 -15
  13. data/app/models/host_status/monitoring_status.rb +7 -4
  14. data/app/models/monitoring_result.rb +13 -6
  15. data/app/models/setting/monitoring.rb +4 -2
  16. data/app/overrides/add_host_monitoring_result_tab.rb +8 -6
  17. data/app/overrides/add_host_multiple_power_set_downtime_checkbox.rb +5 -3
  18. data/app/overrides/add_host_set_downtime_modal.rb +4 -2
  19. data/app/services/monitoring.rb +3 -0
  20. data/app/views/hosts/_downtime_fields.html.erb +2 -2
  21. data/app/views/monitoring_results/_host_tab_pane.html.erb +2 -2
  22. data/config/routes.rb +3 -0
  23. data/db/migrate/20160817135723_create_monitoring_results.rb +5 -1
  24. data/db/migrate/20161220201510_add_monitoring_proxy_id_to_host_and_hostgroup.rb +3 -1
  25. data/db/migrate/201910180900_rename_downtime_host_permission.rb +15 -0
  26. data/db/seeds.d/60-monitoring_proxy_feature.rb +2 -0
  27. data/lib/foreman_monitoring.rb +2 -0
  28. data/lib/foreman_monitoring/engine.rb +32 -28
  29. data/lib/foreman_monitoring/version.rb +3 -1
  30. data/lib/tasks/foreman_monitoring_tasks.rake +5 -5
  31. data/locale/gemspec.rb +2 -0
  32. data/test/controllers/api/v2/downtime_controller_test.rb +73 -0
  33. data/test/factories/feature.rb +4 -2
  34. data/test/factories/host.rb +6 -4
  35. data/test/factories/monitoring_results.rb +9 -7
  36. data/test/factories/smart_proxy.rb +3 -1
  37. data/test/functional/hosts_controller_test.rb +65 -52
  38. data/test/lib/proxy_api/monitoring_test.rb +16 -14
  39. data/test/test_plugin_helper.rb +5 -3
  40. data/test/unit/host_status/monitoring_status_test.rb +18 -16
  41. data/test/unit/host_test.rb +6 -4
  42. data/test/unit/monitoring_result_test.rb +75 -0
  43. data/test/unit/monitoring_test.rb +5 -3
  44. metadata +68 -20
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HostStatus
2
4
  class MonitoringStatus < HostStatus::Status
3
5
  OK = 0
@@ -11,9 +13,10 @@ module HostStatus
11
13
 
12
14
  def to_status(_options = {})
13
15
  state = OK
14
- grouped_results.each do |resultset, _count|
16
+ grouped_results.each_key do |resultset|
15
17
  result, downtime, acknowledged = resultset
16
18
  next if downtime
19
+
17
20
  result = map_result_to_status(result)
18
21
  result = WARNING if acknowledged || result == UNKNOWN
19
22
  state = result if result > state
@@ -23,6 +26,7 @@ module HostStatus
23
26
 
24
27
  def to_global(_options = {})
25
28
  return HostStatus::Global::OK unless should_affect_global_status?
29
+
26
30
  case status
27
31
  when OK
28
32
  HostStatus::Global::OK
@@ -60,9 +64,7 @@ module HostStatus
60
64
  host.monitoring_results.any?
61
65
  end
62
66
 
63
- def host_monitored?
64
- host.monitored?
65
- end
67
+ delegate :monitored?, to: :host, prefix: true
66
68
 
67
69
  def should_affect_global_status?
68
70
  Setting[:monitoring_affect_global_status]
@@ -76,6 +78,7 @@ module HostStatus
76
78
 
77
79
  def map_result_to_status(result)
78
80
  return result if Rails::VERSION::MAJOR < 5
81
+
79
82
  case result.to_sym
80
83
  when :ok
81
84
  OK
@@ -1,10 +1,13 @@
1
- class MonitoringResult < ActiveRecord::Base
2
- enum :result => %i[ok warning critical unknown]
1
+ # frozen_string_literal: true
2
+
3
+ class MonitoringResult < ApplicationRecord
4
+ enum :result => { :ok => 0, :warning => 1, :critical => 2, :unknown => 3 }
3
5
 
4
6
  belongs_to_host
5
7
 
8
+ # rubocop:disable Metrics/AbcSize
6
9
  def self.import(result)
7
- host = Host.find_by_name(result[:host])
10
+ host = Host.find_by(name: result[:host])
8
11
 
9
12
  if host.nil?
10
13
  logger.error "Unable to find host #{result[:host]}"
@@ -27,21 +30,25 @@ class MonitoringResult < ActiveRecord::Base
27
30
  end
28
31
 
29
32
  created = MonitoringResult.where(:host => host, :service => result[:service]).first_or_create
30
- if created.timestamp.blank? || updates[:timestamp].blank? || created.timestamp < updates[:timestamp]
31
- created.update_attributes(updates)
33
+ # rubocop:disable Rails/Date
34
+ if created.timestamp.blank? || updates[:timestamp].blank? || (created.timestamp.to_time - updates[:timestamp].to_time) < 2
35
+ created.update(updates)
32
36
 
33
37
  if created.persisted?
34
38
  logger.info("Imported monitoring result for #{host} in #{(Time.now.utc - start_time).round(2)} seconds")
35
- host.refresh_statuses
39
+ host.get_status(::HostStatus::MonitoringStatus).refresh!
36
40
  end
37
41
  else
38
42
  logger.debug "Skipping monitoring result import for #{host} as it is older than what we have."
39
43
  end
44
+ # rubocop:enable Rails/Date
40
45
  end
46
+ # rubocop:enable Metrics/AbcSize
41
47
 
42
48
  def status
43
49
  return :ok if downtime
44
50
  return :warning if acknowledged
51
+
45
52
  result.to_sym
46
53
  end
47
54
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Setting
2
4
  class Monitoring < ::Setting
3
5
  def self.default_settings
@@ -7,10 +9,10 @@ class Setting
7
9
  true, N_('Monitoring status should affect global status')),
8
10
  set('monitoring_create_action',
9
11
  _('What action should be taken when a host is created'),
10
- 'create', N_('Host Create Action'), nil, { :collection => proc { ::Monitoring::CREATE_ACTIONS } }),
12
+ 'create', N_('Host Create Action'), nil, :collection => proc { ::Monitoring::CREATE_ACTIONS }),
11
13
  set('monitoring_delete_action',
12
14
  _('What action should be taken when a host is deleted'),
13
- 'delete', N_('Host Delete Action'), nil, { :collection => proc { ::Monitoring::DELETE_ACTIONS } })
15
+ 'delete', N_('Host Delete Action'), nil, :collection => proc { ::Monitoring::DELETE_ACTIONS })
14
16
  ]
15
17
  end
16
18
 
@@ -1,9 +1,11 @@
1
- Deface::Override.new(:virtual_path => 'hosts/show',
2
- :name => 'add_monitoring_result_tab',
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(:virtual_path => 'hosts/show',
4
+ :name => 'add_monitoring_result_tab',
3
5
  :insert_bottom => 'ul.nav-tabs',
4
- :partial => 'monitoring_results/host_tab')
6
+ :partial => 'monitoring_results/host_tab')
5
7
 
6
- Deface::Override.new(:virtual_path => 'hosts/show',
7
- :name => 'add_monitoring_result_tab_pane',
8
+ Deface::Override.new(:virtual_path => 'hosts/show',
9
+ :name => 'add_monitoring_result_tab_pane',
8
10
  :insert_bottom => 'div.tab-content',
9
- :partial => 'monitoring_results/host_tab_pane')
11
+ :partial => 'monitoring_results/host_tab_pane')
@@ -1,4 +1,6 @@
1
- Deface::Override.new(:virtual_path => 'hosts/select_multiple_power_state',
2
- :name => 'add_host_multiple_power_set_downtime_checkbox',
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(:virtual_path => 'hosts/select_multiple_power_state',
4
+ :name => 'add_host_multiple_power_set_downtime_checkbox',
3
5
  :insert_before => "erb[silent]:contains('end')",
4
- :partial => 'hosts/host_downtime_checkbox')
6
+ :partial => 'hosts/host_downtime_checkbox')
@@ -1,4 +1,6 @@
1
- Deface::Override.new(:virtual_path => 'hosts/show',
2
- :name => 'add_monitoring_set_downtime_modal',
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(:virtual_path => 'hosts/show',
4
+ :name => 'add_monitoring_set_downtime_modal',
3
5
  :insert_after => 'div#review_before_build',
4
6
  :partial => 'hosts/set_host_downtime')
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Monitoring
2
4
  CREATE_ACTIONS = {
3
5
  'none' => _('None'),
@@ -49,6 +51,7 @@ class Monitoring
49
51
  def query_host(host)
50
52
  result = proxy_api.query_host(host.name)
51
53
  return {} unless result
54
+
52
55
  {
53
56
  :attrs => result
54
57
  }
@@ -1,3 +1,3 @@
1
1
  <%= text_f f, :comment, :size => "col-md-5", :label => _('Comment'), :help_inline => _('Short description that explains why the downtime was set.'), :required => true %>
2
- <%= datetime_f f, :starttime, :size => "col-md-5", :label => _('Starttime'), :help_inline => _('Time when the downtime should start.'), :value => Time.current.strftime("%Y-%m-%dT%H:%M"), :required => true %>
3
- <%= datetime_f f, :endtime, :size => "col-md-5", :label => _('Endtime'), :help_inline => _('Time when the downtime should end.'), :value => Time.current.advance(:hours => 2).strftime("%Y-%m-%dT%H:%M"), :required => true %>
2
+ <%= monitoring_datetime_f f, :starttime, :size => "col-md-5", :label => _('Starttime'), :help_inline => _('Time when the downtime should start.'), :min => DateTime.now, :value => Time.current, :required => true %>
3
+ <%= monitoring_datetime_f f, :endtime, :size => "col-md-5", :label => _('Endtime'), :help_inline => _('Time when the downtime should end.'), :value => Time.current.advance(:hours => 2), :required => true %>
@@ -1,8 +1,8 @@
1
1
  <%- if authorized_for(:permission => 'view_monitoring_results', :auth_object => @host) && @host.monitored? -%>
2
2
  <div class="tab-pane" id="monitoring">
3
3
  <% if @host.monitoring_results.any? %>
4
- <table class="table table-bordered table-striped">
5
- <thead
4
+ <table class="<%= table_css_classes %>">
5
+ <thead>
6
6
  <tr>
7
7
  <th colspan="2">Monitoring</th>
8
8
  </tr>
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Rails.application.routes.draw do
2
4
  namespace :api, :defaults => { :format => 'json' } do
3
5
  scope '(:apiv)', :module => :v2,
@@ -5,6 +7,7 @@ Rails.application.routes.draw do
5
7
  :apiv => /v1|v2/,
6
8
  :constraints => ApiConstraints.new(:version => 2) do
7
9
  resources :monitoring_results, :only => [:create]
10
+ resources :downtime, :only => [:create]
8
11
  end
9
12
  end
10
13
 
@@ -1,5 +1,8 @@
1
- class CreateMonitoringResults < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class CreateMonitoringResults < ActiveRecord::Migration[4.2]
2
4
  def change
5
+ # rubocop:disable Rails/CreateTableWithTimestamps
3
6
  create_table :monitoring_results do |t|
4
7
  t.references :host, :null => false
5
8
  t.string :service, :null => false
@@ -8,5 +11,6 @@ class CreateMonitoringResults < ActiveRecord::Migration
8
11
  t.boolean :acknowledged, :default => false, :null => false
9
12
  t.datetime :timestamp
10
13
  end
14
+ # rubocop:enable Rails/CreateTableWithTimestamps
11
15
  end
12
16
  end
@@ -1,4 +1,6 @@
1
- class AddMonitoringProxyIdToHostAndHostgroup < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class AddMonitoringProxyIdToHostAndHostgroup < ActiveRecord::Migration[4.2]
2
4
  def self.up
3
5
  add_column :hosts, :monitoring_proxy_id, :integer
4
6
  add_column :hostgroups, :monitoring_proxy_id, :integer
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RenameDowntimeHostPermission < ActiveRecord::Migration[5.2]
4
+ def up
5
+ # rubocop:disable Rails/SkipsModelValidations
6
+ Permission.where(name: 'manage_host_downtimes').update_all(name: 'manage_downtime_hosts')
7
+ # rubocop:enable Rails/SkipsModelValidations
8
+ end
9
+
10
+ def down
11
+ # rubocop:disable Rails/SkipsModelValidations
12
+ Permission.where(name: 'manage_downtime_hosts').update_all(name: 'manage_host_downtimes')
13
+ # rubocop:enable Rails/SkipsModelValidations
14
+ end
15
+ end
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  f = Feature.where(:name => 'Monitoring').first_or_create
2
4
  raise "Unable to create proxy feature: #{format_errors f}" if f.nil? || f.errors.any?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'foreman_monitoring/engine'
2
4
 
3
5
  module ForemanMonitoring
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'deface'
2
4
 
3
5
  module ForemanMonitoring
@@ -17,42 +19,44 @@ module ForemanMonitoring
17
19
  end
18
20
 
19
21
  initializer 'foreman_monitoring.load_default_settings',
20
- :before => :load_config_initializers do |_app|
21
- if begin
22
- Setting.table_exists?
23
- rescue
24
- false
25
- end
26
- require_dependency File.expand_path('../../../app/models/setting/monitoring.rb', __FILE__)
27
- end
22
+ :before => :load_config_initializers do |_app|
23
+ setting_table_exists = begin
24
+ Setting.table_exists?
25
+ rescue StandardError
26
+ false
27
+ end
28
+ require_dependency File.expand_path('../../app/models/setting/monitoring.rb', __dir__) if setting_table_exists
28
29
  end
29
30
 
30
31
  initializer 'foreman_monitoring.register_plugin', :before => :finisher_hook do |_app|
31
32
  Foreman::Plugin.register :foreman_monitoring do
32
- requires_foreman '>= 1.15'
33
+ requires_foreman '>= 2.0'
33
34
 
34
35
  apipie_documented_controllers ["#{ForemanMonitoring::Engine.root}/app/controllers/api/v2/*.rb"]
35
36
 
36
37
  security_block :foreman_monitoring do
37
38
  permission :view_monitoring_results,
38
- {},
39
- :resource_type => 'Host'
40
- permission :manage_host_downtimes,
41
- { :hosts => [:downtime, :select_multiple_downtime, :update_multiple_downtime] },
42
- :resource_type => 'Host'
39
+ {},
40
+ :resource_type => 'Host'
41
+ permission :manage_downtime_hosts,
42
+ { :hosts => [:downtime, :select_multiple_downtime, :update_multiple_downtime], :'api/v2/downtime' => [:create] },
43
+ :resource_type => 'Host'
43
44
  permission :upload_monitoring_results,
44
- :'api/v2/monitoring_results' => [:create]
45
- permission :edit_hosts,
46
- { :hosts => [:select_multiple_monitoring_proxy, :update_multiple_monitoring_proxy] },
47
- :resource_type => 'Host'
45
+ :'api/v2/monitoring_results' => [:create]
48
46
  end
49
47
 
50
- role 'Monitoring viewer', [:view_monitoring_results]
51
- role 'Monitoring manager', [:view_monitoring_results, :manage_host_downtimes]
48
+ # Extend built in permissions
49
+ Foreman::AccessControl.permission(:edit_hosts).actions.concat [
50
+ 'hosts/select_multiple_monitoring_proxy',
51
+ 'hosts/update_multiple_monitoring_proxy'
52
+ ]
53
+
54
+ role 'Monitoring viewer', [:view_monitoring_results], 'Role granting permissions to view monitor results'
55
+ role 'Monitoring manager', [:view_monitoring_results, :manage_downtime_hosts], 'Role granting permissions to view monitor results and manage downtimes'
52
56
 
53
57
  register_custom_status HostStatus::MonitoringStatus
54
58
 
55
- add_controller_action_scope(HostsController, :index) { |base_scope| base_scope.includes(:monitoring_results) }
59
+ add_controller_action_scope('HostsController', :index) { |base_scope| base_scope.includes(:monitoring_results) }
56
60
 
57
61
  monitoring_proxy_options = {
58
62
  :feature => 'Monitoring',
@@ -65,23 +69,23 @@ module ForemanMonitoring
65
69
  smart_proxy_for Host::Managed, :monitoring_proxy, monitoring_proxy_options
66
70
  smart_proxy_for Hostgroup, :monitoring_proxy, monitoring_proxy_options
67
71
 
68
- add_controller_action_scope(HostsController, :index) { |base_scope| base_scope.includes(:monitoring_proxy) }
72
+ add_controller_action_scope('HostsController', :index) { |base_scope| base_scope.includes(:monitoring_proxy) }
69
73
  end
70
74
  end
71
75
 
72
76
  config.to_prepare do
73
77
  begin
74
- ::Host::Managed.send(:prepend, ForemanMonitoring::HostExtensions)
75
- ::Hostgroup.send(:include, ForemanMonitoring::HostgroupExtensions)
76
- ::HostsHelper.send(:prepend, ForemanMonitoring::HostsHelperExt)
77
- ::HostsController.send(:prepend, ForemanMonitoring::HostsControllerExtensions)
78
- rescue => e
78
+ ::Host::Managed.prepend(ForemanMonitoring::HostExtensions)
79
+ ::Hostgroup.include(ForemanMonitoring::HostgroupExtensions)
80
+ ::HostsHelper.prepend(ForemanMonitoring::HostsHelperExt)
81
+ ::HostsController.prepend(ForemanMonitoring::HostsControllerExtensions)
82
+ rescue StandardError => e
79
83
  Rails.logger.warn "ForemanMonitoring: skipping engine hook (#{e})"
80
84
  end
81
85
  end
82
86
 
83
87
  initializer 'foreman_monitoring.register_gettext', after: :load_config_initializers do |_app|
84
- locale_dir = File.join(File.expand_path('../../..', __FILE__), 'locale')
88
+ locale_dir = File.join(File.expand_path('../..', __dir__), 'locale')
85
89
  locale_domain = 'foreman_monitoring'
86
90
  Foreman::Gettext::Support.add_text_domain locale_domain, locale_dir
87
91
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ForemanMonitoring
2
- VERSION = '0.1.1'.freeze
4
+ VERSION = '2.0.0'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Tests
2
4
  namespace :test do
3
5
  desc 'Test ForemanMonitoring'
@@ -11,7 +13,7 @@ namespace :test do
11
13
  end
12
14
 
13
15
  namespace :foreman_monitoring do
14
- task :rubocop do
16
+ task rubocop: :environment do
15
17
  begin
16
18
  require 'rubocop/rake_task'
17
19
  RuboCop::RakeTask.new(:rubocop_foreman_monitoring) do |task|
@@ -19,7 +21,7 @@ namespace :foreman_monitoring do
19
21
  "#{ForemanMonitoring::Engine.root}/lib/**/*.rb",
20
22
  "#{ForemanMonitoring::Engine.root}/test/**/*.rb"]
21
23
  end
22
- rescue
24
+ rescue StandardError
23
25
  puts 'Rubocop not loaded.'
24
26
  end
25
27
 
@@ -30,6 +32,4 @@ end
30
32
  Rake::Task[:test].enhance ['test:foreman_monitoring']
31
33
 
32
34
  load 'tasks/jenkins.rake'
33
- if Rake::Task.task_defined?(:'jenkins:unit')
34
- Rake::Task['jenkins:unit'].enhance ['test:foreman_monitoring', 'foreman_monitoring:rubocop']
35
- end
35
+ Rake::Task['jenkins:unit'].enhance ['test:foreman_monitoring', 'foreman_monitoring:rubocop'] if Rake::Task.task_defined?(:'jenkins:unit')
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Matches foreman_monitoring.gemspec
2
4
  _('Set a downtime for hosts after they are deleted in Foreman.')
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ class Api::V2::DowntimeControllerTest < ActionController::TestCase
6
+ let(:host1) { as_admin { FactoryBot.create(:host, :managed) } }
7
+
8
+ context 'with user authentication' do
9
+ context '#create' do
10
+ test 'should deny access' do
11
+ put :create
12
+ assert_response :forbidden
13
+ end
14
+ end
15
+ end
16
+
17
+ context 'with client cert' do
18
+ setup do
19
+ User.current = nil
20
+ reset_api_credentials
21
+
22
+ Setting[:ssl_client_dn_env] = 'SSL_CLIENT_S_DN'
23
+ Setting[:ssl_client_verify_env] = 'SSL_CLIENT_VERIFY'
24
+
25
+ @request.env['HTTPS'] = 'on'
26
+ @request.env['SSL_CLIENT_S_DN'] = "CN=#{host1.name},DN=example,DN=com"
27
+ @request.env['SSL_CLIENT_VERIFY'] = 'SUCCESS'
28
+ end
29
+
30
+ context '#create' do
31
+ test 'should create downtime' do
32
+ put :create
33
+ assert_response :success
34
+ end
35
+ end
36
+
37
+ context '#create with duration' do
38
+ test 'should create downtime with given duration' do
39
+ Host::Managed.any_instance.expects(:downtime_host).with { |params| params[:end_time] - params[:start_time] == 3600 }
40
+ put :create, params: { duration: 3600 }
41
+ assert_response :success
42
+ end
43
+
44
+ test 'should create downtime with given duration as string' do
45
+ Host::Managed.any_instance.expects(:downtime_host).with { |params| params[:end_time] - params[:start_time] == 3600 }
46
+ put :create, params: { duration: '3600' }
47
+ assert_response :success
48
+ end
49
+ end
50
+
51
+ context '#create with reason' do
52
+ test 'should create downtime with given reason' do
53
+ Host::Managed.any_instance.expects(:downtime_host).with { |params| params[:comment] == 'In testing' }
54
+ put :create, params: { reason: 'In testing' }
55
+ assert_response :success
56
+ end
57
+ end
58
+ end
59
+
60
+ context 'without any credentials' do
61
+ setup do
62
+ User.current = nil
63
+ reset_api_credentials
64
+ end
65
+
66
+ context '#create' do
67
+ test 'should deny access' do
68
+ post :create
69
+ assert_response :unauthorized
70
+ end
71
+ end
72
+ end
73
+ end