berta 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +19 -0
- data/Gemfile +3 -0
- data/LICENSE +14 -0
- data/README.md +48 -0
- data/Rakefile +17 -0
- data/berta.gemspec +32 -0
- data/bin/berta +4 -0
- data/config/berta.yml +19 -0
- data/config/email.erb +18 -0
- data/lib/berta.rb +13 -0
- data/lib/berta/cli.rb +108 -0
- data/lib/berta/command_executor.rb +18 -0
- data/lib/berta/entities.rb +6 -0
- data/lib/berta/entities/expiration.rb +82 -0
- data/lib/berta/errors.rb +9 -0
- data/lib/berta/errors/backend_error.rb +5 -0
- data/lib/berta/errors/entities.rb +9 -0
- data/lib/berta/errors/entities/invalid_entity_xml_error.rb +7 -0
- data/lib/berta/errors/entities/no_user_email_error.rb +7 -0
- data/lib/berta/errors/opennebula.rb +13 -0
- data/lib/berta/errors/opennebula/authentication_error.rb +7 -0
- data/lib/berta/errors/opennebula/resource_not_found_error.rb +7 -0
- data/lib/berta/errors/opennebula/resource_retrieval_error.rb +7 -0
- data/lib/berta/errors/opennebula/resource_state_error.rb +7 -0
- data/lib/berta/errors/opennebula/stub_error.rb +7 -0
- data/lib/berta/errors/opennebula/user_not_authorized_error.rb +7 -0
- data/lib/berta/errors/standard_error.rb +5 -0
- data/lib/berta/expiration_manager.rb +42 -0
- data/lib/berta/notification_manager.rb +100 -0
- data/lib/berta/service.rb +118 -0
- data/lib/berta/settings.rb +37 -0
- data/lib/berta/utils.rb +6 -0
- data/lib/berta/utils/opennebula.rb +8 -0
- data/lib/berta/utils/opennebula/helper.rb +47 -0
- data/lib/berta/virtual_machine_handler.rb +120 -0
- metadata +304 -0
data/lib/berta/errors.rb
ADDED
@@ -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,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,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
|
data/lib/berta/utils.rb
ADDED