berta 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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