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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2c35df5b38eb1ec48f721aab12ab14ae6006f32d
4
+ data.tar.gz: 205b376e2b57fb9738880bdd47817b4933135f04
5
+ SHA512:
6
+ metadata.gz: 9500f02e7755d6eab251cb337bf397cb9979f685fc13b736d433756289c3a5da0ba358cd4731f7e58eef955144f93aff8c8598f5e59cceba148a168cb472811a
7
+ data.tar.gz: 3581efd6664b2f26394e7e0bc752c22c01d7e983ea5d113e026044a3df7d65186e34dc305d992ad5accbe1720f75edd8ae110647b6fa795ffa0c77780b6ba466
data/.rubocop.yml ADDED
@@ -0,0 +1,26 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.0
5
+
6
+ Metrics/LineLength:
7
+ Max: 135
8
+
9
+ Metrics/MethodLength:
10
+ Max: 15
11
+
12
+ Metrics/BlockLength:
13
+ Exclude:
14
+ - 'Rakefile'
15
+ - '**/*.rake'
16
+ - 'spec/**/*.rb'
17
+ - '*.gemspec'
18
+ Metrics/AbcSize:
19
+ Exclude:
20
+ - 'lib/berta/cli.rb'
21
+
22
+ RSpec/MultipleExpectations:
23
+ Enabled: false
24
+
25
+ RSpec/ExampleLength:
26
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ sudo: false
2
+
3
+ language: ruby
4
+ rvm:
5
+ - ruby-head
6
+ - 2.2.0
7
+
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: ruby-head
11
+ fast_finish: true
12
+
13
+ branches:
14
+ only:
15
+ - master
16
+
17
+ before_install: 'gem install bundler -v 1.13.0'
18
+
19
+ script: 'bundle exec rake acceptance'
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+
2
+ Copyright 2016 Dusan Baran
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Berta
2
+
3
+ [![Build Status](https://travis-ci.org/dudoslav/berta.svg?branch=master)](https://travis-ci.org/dudoslav/berta)
4
+ [![Coverage Status](https://coveralls.io/repos/github/dudoslav/berta/badge.svg?branch=master)](https://coveralls.io/github/dudoslav/berta?branch=master)
5
+ [![Code Climate](https://codeclimate.com/github/dudoslav/berta/badges/gpa.svg)](https://codeclimate.com/github/dudoslav/berta)
6
+ [![Inline docs](http://inch-ci.org/github/dudoslav/berta.svg?branch=master)](http://inch-ci.org/github/dudoslav/berta)
7
+ [![Dependency Status](https://gemnasium.com/badges/github.com/dudoslav/berta.svg)](https://gemnasium.com/github.com/dudoslav/berta)
8
+
9
+ Berta cleans cloud from unused vms. She sets expiration to all virtual machines
10
+ and when expiration is close she will notify owners. Berta is developed as ruby gem.
11
+
12
+ ## Getting started
13
+
14
+ ### Installation
15
+
16
+ From rubygems:
17
+
18
+ ```bash
19
+ gem install berta
20
+ ```
21
+
22
+ From source:
23
+
24
+ ```bash
25
+ git clone git://github.com/dudoslav/berta.git
26
+ cd cloudkeeper
27
+ gem install bundler
28
+ bundle install
29
+ ```
30
+
31
+ ### Configuration
32
+
33
+ Config files can be located in:
34
+ * `~/.berta/berta.yml`
35
+ * `/etc/berta/berta.yml`
36
+ * `PATH_TO_GEM_DIR/config/cloudkeeper.yml`
37
+
38
+ ### Execution
39
+
40
+ Berta needs access to opennebula backend. To do that she needs to know opennebula
41
+ secret and endpoint. This can be specified in config file, as command line options or
42
+ by creating `one_auth` file in `~/.one`.
43
+ To run berta simply type:
44
+ ```bash
45
+ berta
46
+ # or if backend and secret are not set
47
+ berta --opennebula-secret=<secret> --opennebula-endpoint=<endpoint>
48
+ ```
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rubocop/rake_task'
3
+ require 'bundler/gem_tasks'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :test
9
+
10
+ desc 'Run acceptance tests (RSpec + Rubocop)'
11
+ task test: 'acceptance'
12
+
13
+ desc 'Run acceptance tests (RSpec + Rubocop)'
14
+ task :acceptance do |_t|
15
+ Rake::Task['spec'].invoke
16
+ Rake::Task['rubocop'].invoke
17
+ end
data/berta.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'berta'
3
+ s.version = '1.0.0'
4
+ s.summary = 'Berta VM expiration tool'
5
+ s.description = 'Berta will check all VMs on OpenNebula cloud for expiration date'
6
+ s.authors = ['Dusan Baran']
7
+ s.email = 'dbaran@hotmail.sk'
8
+ s.homepage = 'https://github.com/dudoslav/berta'
9
+ s.license = 'Apache License, Version 2.0'
10
+
11
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
12
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
13
+ s.require_paths = ['lib']
14
+
15
+ s.add_development_dependency 'bundler', '~> 1.13'
16
+ s.add_development_dependency 'rake', '~> 11.2'
17
+ s.add_development_dependency 'rspec', '~> 3.5'
18
+ s.add_development_dependency 'rubocop', '~> 0.42'
19
+ s.add_development_dependency 'rubocop-rspec', '~> 1.7'
20
+ s.add_development_dependency 'pry', '~> 0.10'
21
+ s.add_development_dependency 'webmock', '~> 2.3'
22
+ s.add_development_dependency 'vcr', '~> 3.0'
23
+ s.add_development_dependency 'coveralls', '~> 0.8'
24
+
25
+ s.add_runtime_dependency 'thor', '~> 0.19'
26
+ s.add_runtime_dependency 'settingslogic', '~> 2.0'
27
+ s.add_runtime_dependency 'yell', '~> 2.0'
28
+ s.add_runtime_dependency 'opennebula', '~> 5.2'
29
+ s.add_runtime_dependency 'chronic_duration', '~> 0.10'
30
+ s.add_runtime_dependency 'mail', '~> 2.6'
31
+ s.add_runtime_dependency 'tilt', '~> 2.0'
32
+ end
data/bin/berta ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'berta'
3
+
4
+ Berta::CLI.start(ARGV)
data/config/berta.yml ADDED
@@ -0,0 +1,19 @@
1
+ berta:
2
+ opennebula:
3
+ secret: # Secret for OpenNebula authentication
4
+ endpoint: # OpenNebula backend endpoint
5
+ expiration: # Action that will be set for execution
6
+ offset: 10 days # Time until action will be executed
7
+ action: terminate-hard # Type of action that will be executed
8
+ notification:
9
+ deadline: 1 day # Time when user should be notified before action will be executed
10
+ exclude: # Exclude VMs to ignore them
11
+ ids: # VMs with this IDs will be ignored
12
+ users: # VMs owned by this users will be ignored
13
+ groups: # VMs in this groups will be ignored
14
+ clusters: # VMs in this clusters will be ignored
15
+ logging:
16
+ file: /var/log/berta/berta.log # File ro write log to. To turn off file logging leave this field empty
17
+ level: error # Logging level
18
+ dry-run: false # Berta wont change anything on running instance of OpenNebula
19
+ debug: false # Debug mode
data/config/email.erb ADDED
@@ -0,0 +1,18 @@
1
+ <%# Avaliable parametes: %>
2
+ <%# [String] user_name %>
3
+ <%# [String] user_email %>
4
+ <%# [Array<Hash>] vms, Array of Hashes with values: %>
5
+ <%# [String] id %>
6
+ <%# [String] name %>
7
+ <%# [String] expiration %>
8
+ From: cesnet@mail.muni.cz
9
+ To: <%= user_email %>
10
+ Subject: OpenNebula VM expiration
11
+
12
+ Hi <%= user_name %>,
13
+ your vms are about to expire..
14
+
15
+ VM Name | Expiration Date
16
+ <% vms.each do |vm| %>
17
+ <%= vm[:name] %> | <%= Time.at(vm[:expiration]) %>
18
+ <% end %>
data/lib/berta.rb ADDED
@@ -0,0 +1,13 @@
1
+ # Main Berta module
2
+ module Berta
3
+ autoload :Service, 'berta/service'
4
+ autoload :VirtualMachineHandler, 'berta/virtual_machine_handler'
5
+ autoload :ExpirationManager, 'berta/expiration_manager'
6
+ autoload :NotificationManager, 'berta/notification_manager'
7
+ autoload :CommandExecutor, 'berta/command_executor'
8
+ autoload :CLI, 'berta/cli'
9
+ autoload :Errors, 'berta/errors'
10
+ autoload :Entities, 'berta/entities'
11
+ autoload :Utils, 'berta/utils'
12
+ autoload :Settings, 'berta/settings'
13
+ end
data/lib/berta/cli.rb ADDED
@@ -0,0 +1,108 @@
1
+ require 'thor'
2
+ require 'yell'
3
+
4
+ module Berta
5
+ # CLI for berta
6
+ class CLI < Thor
7
+ def self.safe_fetch(keys)
8
+ current = Berta::Settings
9
+ keys.each do |key|
10
+ current = current[key]
11
+ break unless current
12
+ end
13
+ current
14
+ end
15
+
16
+ class_option :'opennebula-secret',
17
+ default: safe_fetch(%w(opennebula secret)),
18
+ type: :string
19
+ class_option :'opennebula-endpoint',
20
+ default: safe_fetch(%w(opennebula endpoint)),
21
+ type: :string
22
+ class_option :'expiration-offset',
23
+ required: true,
24
+ default: safe_fetch(%w(expiration offset)),
25
+ type: :string
26
+ class_option :'expiration-action',
27
+ required: true,
28
+ default: safe_fetch(%w(expiration action)),
29
+ type: :string
30
+ class_option :'notification-deadline',
31
+ required: true,
32
+ default: safe_fetch(%w(notification deadline)),
33
+ type: :string
34
+ class_option :'exclude-ids',
35
+ default: safe_fetch(%w(exclude ids)),
36
+ type: :array
37
+ class_option :'exclude-users',
38
+ default: safe_fetch(%w(exclude users)),
39
+ type: :array
40
+ class_option :'exclude-groups',
41
+ default: safe_fetch(%w(exclude groups)),
42
+ type: :array
43
+ class_option :'exclude-clusters',
44
+ default: safe_fetch(%w(exclude clusters)),
45
+ type: :array
46
+ class_option :'dry-run',
47
+ default: safe_fetch(%w(dry-run)),
48
+ type: :boolean
49
+ class_option :'logging-file',
50
+ default: safe_fetch(%w(logging file)),
51
+ type: :string
52
+ class_option :'logging-level',
53
+ required: true,
54
+ default: safe_fetch(%w(logging level)),
55
+ type: :string
56
+ class_option :debug,
57
+ default: safe_fetch(%w(debug)),
58
+ type: :boolean
59
+
60
+ desc 'cleanup', 'Task that sets all expiration to all vms and notifies users'
61
+ def cleanup
62
+ initialize_configuration(options)
63
+ initialize_logger(options)
64
+ Berta::CommandExecutor.cleanup
65
+ end
66
+ default_task :cleanup
67
+
68
+ private
69
+
70
+ def initialize_configuration(options)
71
+ settings = Hash.new { |hash, key| hash[key] = {} }
72
+ settings['opennebula']['secret'] = options['opennebula-secret']
73
+ settings['opennebula']['endpoint'] = options['opennebula-endpoint']
74
+ settings['expiration']['offset'] = options['expiration-offset']
75
+ settings['expiration']['action'] = options['expiration-action']
76
+ settings['notification']['deadline'] = options['notification-deadline']
77
+ settings['exclude']['ids'] = options['exclude-ids']
78
+ settings['exclude']['users'] = options['exclude-users']
79
+ settings['exclude']['groups'] = options['exclude-groups']
80
+ settings['exclude']['clusters'] = options['exclude-clusters']
81
+ settings['dry-run'] = options['dry-run']
82
+ settings['debug'] = options['debug']
83
+ settings['logging']['file'] = options['logging-file']
84
+ settings['logging']['level'] = options['logging-level']
85
+ Berta::Settings.merge!(settings)
86
+ end
87
+
88
+ def initialize_logger(options)
89
+ logging_level = options['logging-level']
90
+ logging_level = 'debug' if options['debug'] || options['dry-run']
91
+
92
+ Yell.new :stdout, name: Object, level: logging_level, format: Yell::DefaultFormat
93
+ Object.send :include, Yell::Loggable
94
+
95
+ setup_file_logger(options['logging-file']) if options['logging-file']
96
+
97
+ logger.debug 'Running in debug mode...'
98
+ end
99
+
100
+ def setup_file_logger(logging_file)
101
+ unless (File.exist?(logging_file) && File.writable?(logging_file)) || File.writable?(File.dirname(logging_file))
102
+ logger.error "File #{logging_file} isn't writable"
103
+ return
104
+ end
105
+ logger.adapter :file, logging_file
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,18 @@
1
+ module Berta
2
+ # Class for executing main berta commands
3
+ class CommandExecutor
4
+ # Function that performs clean up operation.
5
+ # Connects to opennebula database,
6
+ # runs expiration update process and
7
+ # notifies users about upcoming expirations.
8
+ def self.cleanup
9
+ service = Berta::Service.new(Berta::Settings['opennebula']['secret'],
10
+ Berta::Settings['opennebula']['endpoint'])
11
+ vms = service.running_vms
12
+ Berta::ExpirationManager.new.update_expirations(vms)
13
+ Berta::NotificationManager.new(service).notify_users(vms)
14
+ rescue Berta::Errors::BackendError => e
15
+ logger.error e.message
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ module Berta
2
+ # Module for entity classes
3
+ module Entities
4
+ autoload :Expiration, 'berta/entities/expiration.rb'
5
+ end
6
+ end
@@ -0,0 +1,82 @@
1
+ module Berta
2
+ module Entities
3
+ # Class for storing expiration data, also can be created from xml
4
+ class Expiration
5
+ attr_reader :id, :time, :action
6
+
7
+ # Creates Expiration class instance from XMLElement.
8
+ #
9
+ # @param xml [XMLElement] XML from which to create instance
10
+ # @return [Berta::Entities::Expiration] Expiration from given xml
11
+ # @raise [Berta::Errors::Entities::InvalidEntityXMLError] If XMLElement was not in correct format
12
+ def self.from_xml(xml)
13
+ check_xml!(xml)
14
+ Berta::Entities::Expiration.new(xml['ID'], xml['TIME'], xml['ACTION'])
15
+ end
16
+
17
+ # Raises error if XML element is not in correct format.
18
+ #
19
+ # @param xml [XMLElement] XML to check for all required values
20
+ # @raise [Berta::Errors::Entities::InvalidEntityXMLError] If xml is not in correct format
21
+ def self.check_xml!(xml)
22
+ raise Berta::Errors::Entities::InvalidEntityXMLError, 'wrong enxpiration xml recieved' \
23
+ unless %w(ID
24
+ ACTION
25
+ TIME).all? { |path| xml.has_elements? path }
26
+ end
27
+
28
+ # Creates expiration class instance from given arguments.
29
+ #
30
+ # @param id [String] Schelude action id
31
+ # @param time [String] Schelude action execution time
32
+ # @param action [String] Schelude action
33
+ def initialize(id, time, action)
34
+ @id = id
35
+ @time = time
36
+ @action = action
37
+ end
38
+
39
+ # Generate schelude action template that can be used
40
+ # in VMS USER_TEMPLATE/SCHED_ACTION
41
+ #
42
+ # @return [String] Schelude action template
43
+ def template
44
+ <<-EOT
45
+ SCHED_ACTION = [
46
+ ID = "#{id}",
47
+ ACTION = "#{action}",
48
+ TIME = "#{time}"
49
+ ]
50
+ EOT
51
+ end
52
+
53
+ # Determines if this schelude action is in notification interval.
54
+ # That means its expiration is closer to Time.now than notification
55
+ # deadline.
56
+ #
57
+ # @return [Boolean] Truthy if in notification interval else falsy
58
+ def in_notification_interval?
59
+ time_interval = time.to_i - Time.now.to_i
60
+ time_interval <= Berta::Settings.notification_deadline && time_interval >= 0
61
+ end
62
+
63
+ # Determines if this schelude action is in expiration interval.
64
+ # That means its expiration is closer to Time.now that expiration
65
+ # offset.
66
+ #
67
+ # @return [Boolean] Truthy if in expiration interval else falsy
68
+ def in_expiration_interval?
69
+ time_interval = time.to_i - Time.now.to_i
70
+ time_interval <= Berta::Settings.expiration_offset && time_interval >= 0
71
+ end
72
+
73
+ # Determines if this expiration has default expiration action set.
74
+ # Default expiration action is action defined in settings file.
75
+ #
76
+ # @return [Boolean] Truthy if has default action else falsy
77
+ def default_action?
78
+ action == Berta::Settings.expiration.action
79
+ end
80
+ end
81
+ end
82
+ end