berta 1.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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +26 -0
  3. data/.travis.yml +19 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +14 -0
  6. data/README.md +48 -0
  7. data/Rakefile +17 -0
  8. data/berta.gemspec +32 -0
  9. data/bin/berta +4 -0
  10. data/config/berta.yml +19 -0
  11. data/config/email.erb +18 -0
  12. data/lib/berta.rb +13 -0
  13. data/lib/berta/cli.rb +108 -0
  14. data/lib/berta/command_executor.rb +18 -0
  15. data/lib/berta/entities.rb +6 -0
  16. data/lib/berta/entities/expiration.rb +82 -0
  17. data/lib/berta/errors.rb +9 -0
  18. data/lib/berta/errors/backend_error.rb +5 -0
  19. data/lib/berta/errors/entities.rb +9 -0
  20. data/lib/berta/errors/entities/invalid_entity_xml_error.rb +7 -0
  21. data/lib/berta/errors/entities/no_user_email_error.rb +7 -0
  22. data/lib/berta/errors/opennebula.rb +13 -0
  23. data/lib/berta/errors/opennebula/authentication_error.rb +7 -0
  24. data/lib/berta/errors/opennebula/resource_not_found_error.rb +7 -0
  25. data/lib/berta/errors/opennebula/resource_retrieval_error.rb +7 -0
  26. data/lib/berta/errors/opennebula/resource_state_error.rb +7 -0
  27. data/lib/berta/errors/opennebula/stub_error.rb +7 -0
  28. data/lib/berta/errors/opennebula/user_not_authorized_error.rb +7 -0
  29. data/lib/berta/errors/standard_error.rb +5 -0
  30. data/lib/berta/expiration_manager.rb +42 -0
  31. data/lib/berta/notification_manager.rb +100 -0
  32. data/lib/berta/service.rb +118 -0
  33. data/lib/berta/settings.rb +37 -0
  34. data/lib/berta/utils.rb +6 -0
  35. data/lib/berta/utils/opennebula.rb +8 -0
  36. data/lib/berta/utils/opennebula/helper.rb +47 -0
  37. data/lib/berta/virtual_machine_handler.rb +120 -0
  38. metadata +304 -0
@@ -0,0 +1,9 @@
1
+ module Berta
2
+ # Module for Berta error classes
3
+ module Errors
4
+ autoload :StandardError, 'berta/errors/standard_error'
5
+ autoload :BackendError, 'berta/errors/backend_error'
6
+ autoload :OpenNebula, 'berta/errors/opennebula'
7
+ autoload :Entities, 'berta/errors/entities'
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Berta
2
+ module Errors
3
+ class BackendError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Berta
2
+ module Errors
3
+ # Module for entity errors
4
+ module Entities
5
+ autoload :InvalidEntityXMLError, 'berta/errors/entities/invalid_entity_xml_error'
6
+ autoload :NoUserEmailError, 'berta/errors/entities/no_user_email_error'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module Entities
4
+ class InvalidEntityXMLError < Berta::Errors::StandardError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module Entities
4
+ class NoUserEmailError < Berta::Errors::StandardError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Berta
2
+ module Errors
3
+ # Module for OpenNebula error classes
4
+ module OpenNebula
5
+ autoload :StubError, 'berta/errors/opennebula/stub_error'
6
+ autoload :AuthenticationError, 'berta/errors/opennebula/authentication_error'
7
+ autoload :UserNotAuthorizedError, 'berta/errors/opennebula/user_not_authorized_error'
8
+ autoload :ResourceNotFoundError, 'berta/errors/opennebula/resource_not_found_error'
9
+ autoload :ResourceStateError, 'berta/errors/opennebula/resource_state_error'
10
+ autoload :ResourceRetrievalError, 'berta/errors/opennebula/resource_retrieval_error'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module OpenNebula
4
+ class AuthenticationError < Berta::Errors::BackendError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module OpenNebula
4
+ class ResourceNotFoundError < Berta::Errors::BackendError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module OpenNebula
4
+ class ResourceRetrievalError < Berta::Errors::BackendError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module OpenNebula
4
+ class ResourceStateError < Berta::Errors::BackendError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module OpenNebula
4
+ class StubError < Berta::Errors::BackendError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Berta
2
+ module Errors
3
+ module OpenNebula
4
+ class UserNotAuthorizedError < Berta::Errors::BackendError; end
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Berta
2
+ module Errors
3
+ class StandardError < ::StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,42 @@
1
+ module Berta
2
+ # Class for managing expiration dates on vms
3
+ class ExpirationManager
4
+ # Update all expirations on vm, removes invalid expirations
5
+ # and if needed will set default expiration date.
6
+ #
7
+ # @param vms [Array<Berta::VirtualMachineHandler>] Virtual machines
8
+ # to update expiration on.
9
+ def update_expirations(vms)
10
+ vms.each do |vm|
11
+ remove_invalid_expirations(vm)
12
+ add_default_expiration(vm)
13
+ end
14
+ end
15
+
16
+ # Removes invalid expirations on vm. That are schelude actions
17
+ # with expiration time later than expiration offset.
18
+ #
19
+ # @param vm [Berta::VirtualMachineHandler] Virtual machine to
20
+ # remove invalid expirations on.
21
+ def remove_invalid_expirations(vm)
22
+ exps = vm.expirations
23
+ exps.keep_if(&:in_expiration_interval?)
24
+ vm.update_expirations(exps) if exps.length != vm.expirations.length
25
+ rescue Berta::Errors::BackendError => e
26
+ logger.error "#{e.message}\n\tOn vm with id #{vm.handle['ID']}"
27
+ end
28
+
29
+ # Adds default expiration if no valid expiration with
30
+ # right expiration action is set.
31
+ #
32
+ # @param vm [Berta::VirtualMachineHandler] Virtual machine
33
+ # to set default expiration on.
34
+ def add_default_expiration(vm)
35
+ return if vm.default_expiration
36
+ vm.add_expiration(Time.now.to_i + Berta::Settings.expiration_offset,
37
+ Berta::Settings.expiration.action)
38
+ rescue Berta::Errors::BackendError => e
39
+ logger.error "#{e.message}\n\tOn vm with id #{vm.handle['ID']}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,100 @@
1
+ require 'mail'
2
+ require 'erb'
3
+ require 'tilt'
4
+
5
+ module Berta
6
+ # Class for managing notifications, setting and sending them
7
+ class NotificationManager
8
+ attr_reader :service, :email_template
9
+
10
+ # Creates NotificationManager object with given service.
11
+ # Notification manager needs service for fetching user
12
+ # data from opennebula database. Also initializes
13
+ # email template object.
14
+ #
15
+ # @param service [Berta::Service] Service that will be used
16
+ # for fetching data.
17
+ def initialize(service)
18
+ @service = service
19
+ email_file = 'email.erb'.freeze
20
+ email_template_path = "#{File.dirname(__FILE__)}/../../config/#{email_file}"
21
+ email_template_path = "etc/berta/#{email_file}" \
22
+ if File.exist?("etc/berta/#{email_file}")
23
+ email_template_path = "#{ENV['HOME']}/.berta/#{email_file}" \
24
+ if File.exist?("#{ENV['HOME']}/.berta/#{email_file}")
25
+ @email_template = Tilt.new(email_template_path)
26
+ end
27
+
28
+ # Notifies users. Finds all users that should be notified
29
+ # and sends email to each of them. Email is generated
30
+ # from template.
31
+ #
32
+ # @param vms [Array<Berta::VirtualMachineHandler>] Virtual machines
33
+ # to check for notifications.
34
+ def notify_users(vms)
35
+ begin
36
+ users = service.users
37
+ rescue Berta::Errors::BackendError => e
38
+ logger.error e.message
39
+ return
40
+ end
41
+ uids_to_notify(vms).each do |uid, uvms|
42
+ user = users.find { |usr| usr['ID'] == uid }
43
+ notify_user(user, uvms) if user
44
+ end
45
+ end
46
+
47
+ # Notifies given user about given vms.
48
+ #
49
+ # @param user [OpenNebula::User] User to notify
50
+ # @param user_vms [Array<Berta::VirtualMachineHandler>] VMs to notify about
51
+ def notify_user(user, user_vms)
52
+ send_notification(user, user_vms)
53
+ rescue ArgumentError, Berta::Errors::Entities::NoUserEmailError => e
54
+ logger.error e.message
55
+ else
56
+ user_vms.each(&:update_notified)
57
+ end
58
+
59
+ # Finds and return uids of users from vms that should be notified.
60
+ #
61
+ # @param vms [Array<VirtualMachineHandler>] VMs to check for notification
62
+ # @return [Hash<String, Array<VirtualMachineHandler>>] Hash of user ids to
63
+ # vms that user with key id should be notified about
64
+ def uids_to_notify(vms)
65
+ notif = vms.keep_if(&:should_notify?)
66
+ uidsvm = Hash.new([])
67
+ notif.each { |vm| uidsvm[vm.handle['UID']] += [vm] }
68
+ uidsvm
69
+ end
70
+
71
+ # Sends email to given user about given vms using sendmail. Email
72
+ # is generated from email template.
73
+ #
74
+ # @param user [OpenNebula::User] User to notify
75
+ # @param vms [Array<Berta::VirtualMachineHandler>] VMs to notify user about
76
+ # @raise [Berta::Errors::Entities::NoUserEmailError] If user has no email set
77
+ def send_notification(user, vms)
78
+ user_email = user['TEMPLATE/EMAIL']
79
+ user_name = user['NAME']
80
+ raise Berta::Errors::Entities::NoUserEmailError, "User: #{user_name} with id: #{user['ID']} has no email set" \
81
+ unless user_email
82
+ mail = Mail.new(email_template.render(Hash,
83
+ user_email: user_email,
84
+ user_name: user_name,
85
+ vms: vms_data(vms)))
86
+ mail.delivery_method :sendmail
87
+ mail.deliver
88
+ end
89
+
90
+ private
91
+
92
+ def vms_data(vms)
93
+ vms.map do |vm|
94
+ { id: vm.handle['ID'],
95
+ name: vm.handle['NAME'],
96
+ expiration: vm.default_expiration.time.to_i }
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,118 @@
1
+ require 'opennebula'
2
+
3
+ module Berta
4
+ # Berta service for communication with OpenNebula
5
+ class Service
6
+ # VM states that take resources
7
+ RESOURCE_STATES = %w(SUSPENDED POWEROFF CLONING).freeze
8
+ # Active state has some lcm states that should not expire
9
+ ACTIVE_STATE = 'ACTIVE'.freeze
10
+ # LCM states in which active state shouldn't expire
11
+ NON_RESOURCE_ACTIVE_LCM_STATES = %w(EPILOG SHUTDOWN STOP UNDEPLOY FAILURE).freeze
12
+
13
+ attr_reader :endpoint
14
+ attr_reader :client
15
+
16
+ # Initializes service object and connects to opennebula
17
+ # backend. If both arguments are nil default ONE_AUTH
18
+ # will be used.
19
+ #
20
+ # @param secret [String] Opennebula secret
21
+ # @param endpoint [String] Endpoint of OpenNebula
22
+ def initialize(secret, endpoint)
23
+ @endpoint = endpoint
24
+ @client = OpenNebula::Client.new(secret, endpoint)
25
+ end
26
+
27
+ # Fetch running vms from OpenNebula and filter out vms that
28
+ # take no resources.
29
+ #
30
+ # @return [Berta::VirtualMachineHandler] Virtual machines
31
+ # running on OpenNebula
32
+ # @raise [Berta::Errors::OpenNebula::AuthenticationError]
33
+ # @raise [Berta::Errors::OpenNebula::UserNotAuthorizedError]
34
+ # @raise [Berta::Errors::OpenNebula::ResourceNotFoundError]
35
+ # @raise [Berta::Errors::OpenNebula::ResourceStateError]
36
+ # @raise [Berta::Errors::OpenNebula::ResourceRetrievalError]
37
+ def running_vms
38
+ logger.debug 'Fetching vms'
39
+ vm_pool = OpenNebula::VirtualMachinePool.new(client)
40
+ Berta::Utils::OpenNebula::Helper.handle_error { vm_pool.info_all }
41
+ vm_pool.map { |vm| Berta::VirtualMachineHandler.new(vm) }
42
+ .delete_if { |vmh| excluded?(vmh) || !takes_resources?(vmh) }
43
+ end
44
+
45
+ # Fetch users from OpenNebula
46
+ #
47
+ # @return [OpenNebula::UserPool] Users on OpenNebula
48
+ # @raise [Berta::Errors::OpenNebula::AuthenticationError]
49
+ # @raise [Berta::Errors::OpenNebula::UserNotAuthorizedError]
50
+ # @raise [Berta::Errors::OpenNebula::ResourceNotFoundError]
51
+ # @raise [Berta::Errors::OpenNebula::ResourceStateError]
52
+ # @raise [Berta::Errors::OpenNebula::ResourceRetrievalError]
53
+ def users
54
+ logger.debug 'Fetching users'
55
+ user_pool = OpenNebula::UserPool.new(client)
56
+ Berta::Utils::OpenNebula::Helper.handle_error { user_pool.info }
57
+ user_pool
58
+ end
59
+
60
+ # Fetch clusters from OpenNebula
61
+ #
62
+ # @return [OpenNebula::ClusterPool] Clusters on OpenNebula
63
+ # @raise [Berta::Errors::OpenNebula::AuthenticationError]
64
+ # @raise [Berta::Errors::OpenNebula::UserNotAuthorizedError]
65
+ # @raise [Berta::Errors::OpenNebula::ResourceNotFoundError]
66
+ # @raise [Berta::Errors::OpenNebula::ResourceStateError]
67
+ # @raise [Berta::Errors::OpenNebula::ResourceRetrievalError]
68
+ def clusters
69
+ logger.debug 'Fetching clusters'
70
+ cluster_pool = OpenNebula::ClusterPool.new(client)
71
+ Berta::Utils::OpenNebula::Helper.handle_error { cluster_pool.info }
72
+ cluster_pool
73
+ end
74
+
75
+ private
76
+
77
+ def excluded?(vmh)
78
+ excluded_id?(vmh) ||
79
+ excluded_user?(vmh) ||
80
+ excluded_group?(vmh) ||
81
+ excluded_cluster?(vmh)
82
+ end
83
+
84
+ def excluded_id?(vmh)
85
+ Berta::Settings.exclude.ids.find { |id| vmh.handle.id == id } \
86
+ if vmh.handle.id && Berta::Settings.exclude.ids
87
+ end
88
+
89
+ def excluded_user?(vmh)
90
+ Berta::Settings.exclude.users.find { |user| vmh.handle['UNAME'] == user } \
91
+ if vmh.handle['UNAME'] && Berta::Settings.exclude.users
92
+ end
93
+
94
+ def excluded_group?(vmh)
95
+ Berta::Settings.exclude.groups.find { |group| vmh.handle['GNAME'] == group } \
96
+ if vmh.handle['GNAME'] && Berta::Settings.exclude.groups
97
+ end
98
+
99
+ def excluded_cluster?(vmh)
100
+ return unless Berta::Settings.exclude.clusters
101
+ vmcid = latest_cluster_id(vmh)
102
+ vmcluster = clusters.find { |cluster| cluster['ID'] == vmcid }
103
+ return unless vmcluster
104
+ Berta::Settings.exclude.clusters.find { |name| vmcluster['NAME'] == name } \
105
+ if vmcluster['NAME']
106
+ end
107
+
108
+ def latest_cluster_id(vmh)
109
+ vmh.handle['HISTORY_RECORDS/HISTORY[last()]/CID']
110
+ end
111
+
112
+ def takes_resources?(vmh)
113
+ return true if RESOURCE_STATES.any? { |state| vmh.handle.state_str == state }
114
+ return true if vmh.handle.state_str == ACTIVE_STATE &&
115
+ NON_RESOURCE_ACTIVE_LCM_STATES.none? { |state| vmh.handle.lcm_state_str.include? state }
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,37 @@
1
+ require 'settingslogic'
2
+ require 'chronic_duration'
3
+
4
+ module Berta
5
+ # Class for storing setting for Berta
6
+ class Settings < Settingslogic
7
+ CONFIGURATION = 'berta.yml'.freeze
8
+
9
+ source "#{ENV['HOME']}/.berta/#{CONFIGURATION}"\
10
+ if File.exist?("#{ENV['HOME']}/.berta/#{CONFIGURATION}")
11
+
12
+ source "/etc/berta/#{CONFIGURATION}"\
13
+ if File.exist?("/etc/berta/#{CONFIGURATION}")
14
+
15
+ source "#{File.dirname(__FILE__)}/../../config/#{CONFIGURATION}"
16
+
17
+ namespace 'berta'
18
+
19
+ # Notification deadline can be written in settings file in human
20
+ # readable form. This function will return notification deadline
21
+ # value as integer.
22
+ #
23
+ # @return [Numeric] Notification deadline
24
+ def self.notification_deadline
25
+ ChronicDuration.parse(get('notification.deadline'))
26
+ end
27
+
28
+ # Expiration offset can be written in settings file in human
29
+ # readable form. This function will return expiration offset
30
+ # value as integer.
31
+ #
32
+ # @return [Numeric] Expiration offset
33
+ def self.expiration_offset
34
+ ChronicDuration.parse(get('expiration.offset'))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ module Berta
2
+ # Utility classes
3
+ module Utils
4
+ autoload :OpenNebula, 'berta/utils/opennebula'
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Berta
2
+ module Utils
3
+ # Module for OpenNebula util classes
4
+ module OpenNebula
5
+ autoload :Helper, 'berta/utils/opennebula/helper'
6
+ end
7
+ end
8
+ end