oneacct-export 0.1.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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +11 -0
  6. data/Rakefile +18 -0
  7. data/bin/oneacct-export +72 -0
  8. data/config/conf.yml +53 -0
  9. data/config/sidekiq.yml +6 -0
  10. data/lib/errors.rb +7 -0
  11. data/lib/errors/authentication_error.rb +3 -0
  12. data/lib/errors/resource_not_found_error.rb +3 -0
  13. data/lib/errors/resource_retrieval_error.rb +3 -0
  14. data/lib/errors/resource_state_error.rb +3 -0
  15. data/lib/errors/user_not_authorized_error.rb +3 -0
  16. data/lib/input_validator.rb +18 -0
  17. data/lib/one_data_accessor.rb +147 -0
  18. data/lib/one_worker.rb +181 -0
  19. data/lib/one_writer.rb +51 -0
  20. data/lib/oneacct_exporter.rb +88 -0
  21. data/lib/oneacct_exporter/log.rb +15 -0
  22. data/lib/oneacct_exporter/version.rb +3 -0
  23. data/lib/oneacct_opts.rb +131 -0
  24. data/lib/redis_conf.rb +29 -0
  25. data/lib/settings.rb +13 -0
  26. data/lib/sidekiq_conf.rb +11 -0
  27. data/lib/templates/apel-0.2.erb +30 -0
  28. data/mock/one_worker_DEPLOY_ID_missing.xml +136 -0
  29. data/mock/one_worker_DISK_missing.xml +119 -0
  30. data/mock/one_worker_ETIME_0.xml +137 -0
  31. data/mock/one_worker_ETIME_missing.xml +136 -0
  32. data/mock/one_worker_ETIME_nan.xml +137 -0
  33. data/mock/one_worker_GID_missing.xml +136 -0
  34. data/mock/one_worker_GNAME_missing.xml +136 -0
  35. data/mock/one_worker_HISTORY_RECORDS_missing.xml +91 -0
  36. data/mock/one_worker_HISTORY_many.xml +137 -0
  37. data/mock/one_worker_HISTORY_missing.xml +93 -0
  38. data/mock/one_worker_HISTORY_one.xml +115 -0
  39. data/mock/one_worker_IMAGE_ID_missing.xml +136 -0
  40. data/mock/one_worker_MEMORY_0.xml +137 -0
  41. data/mock/one_worker_MEMORY_missing.xml +136 -0
  42. data/mock/one_worker_MEMORY_nan.xml +137 -0
  43. data/mock/one_worker_NET_RX_0.xml +137 -0
  44. data/mock/one_worker_NET_RX_missing.xml +136 -0
  45. data/mock/one_worker_NET_RX_nan.xml +137 -0
  46. data/mock/one_worker_NET_TX_0.xml +137 -0
  47. data/mock/one_worker_NET_TX_missing.xml +136 -0
  48. data/mock/one_worker_NET_TX_nan.xml +137 -0
  49. data/mock/one_worker_RETIME_0.xml +115 -0
  50. data/mock/one_worker_RETIME_missing.xml +114 -0
  51. data/mock/one_worker_RSTIME_0.xml +115 -0
  52. data/mock/one_worker_RSTIME_>_RETIME.xml +115 -0
  53. data/mock/one_worker_RSTIME_missing.xml +114 -0
  54. data/mock/one_worker_STATE_missing.xml +136 -0
  55. data/mock/one_worker_STATE_out_of_range.xml +137 -0
  56. data/mock/one_worker_STIME_>_ETIME.xml +137 -0
  57. data/mock/one_worker_STIME_missing.xml +136 -0
  58. data/mock/one_worker_STIME_nan.xml +137 -0
  59. data/mock/one_worker_TEMPLATE_missing.xml +79 -0
  60. data/mock/one_worker_UID_missing.xml +136 -0
  61. data/mock/one_worker_VCPU_0.xml +137 -0
  62. data/mock/one_worker_VCPU_missing.xml +136 -0
  63. data/mock/one_worker_VCPU_nan.xml +137 -0
  64. data/mock/one_worker_malformed_vm.xml +136 -0
  65. data/mock/one_worker_valid_machine.xml +137 -0
  66. data/mock/one_worker_vm1.xml +137 -0
  67. data/mock/one_worker_vm2.xml +137 -0
  68. data/mock/one_worker_vm3.xml +137 -0
  69. data/mock/one_writer_testfile +2 -0
  70. data/oneacct-export.gemspec +31 -0
  71. data/spec/one_data_accessor_spec.rb +441 -0
  72. data/spec/one_worker_spec.rb +684 -0
  73. data/spec/one_writer_spec.rb +146 -0
  74. data/spec/oneacct_exporter_spec.rb +262 -0
  75. data/spec/oneacct_opts_spec.rb +229 -0
  76. data/spec/redis_conf_spec.rb +94 -0
  77. data/spec/spec_helper.rb +11 -0
  78. metadata +254 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 670e8800aaf7a64b5f75d0f6d930e0d9be4d96fb
4
+ data.tar.gz: e6545c409b1c10e466a927833d3ffcde5b0c9c19
5
+ SHA512:
6
+ metadata.gz: 79f668da9123a2b2d83eb3131062494614ce605132b394921b0867faeb72c53c7a16649095e31928f6e59eb0163b4e78574670515cd928dfbaf75914bdf708bc
7
+ data.tar.gz: fe9e971c3b06d3f7717e24feca0379206f4e8123276d01815618c475b331fbcf2e30db1bad2cbfa19b0190615ff3d759950d2f097c25bd9e913596f15c822fa2
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.bundle
10
+ *.so
11
+ *.o
12
+ *.a
13
+ mkmf.log
14
+ *.gem
15
+ *.swp
16
+ *.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in oneacct_export.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # OneacctExport
2
+
3
+ Exporting OpenNebula accounting data.
4
+
5
+ ## Contributing
6
+
7
+ 1. Fork it ( https://github.com/Misenko/oneacct_export/fork )
8
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
9
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
10
+ 4. Push to the branch (`git push origin my-new-feature`)
11
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubygems/tasks'
3
+
4
+ task :default => 'test'
5
+
6
+ desc "Run all tests; includes rspec and coverage reports"
7
+ task :test => 'rcov:rspec'
8
+
9
+ desc "Run all tests; includes rspec and coverage reports"
10
+ task :spec => 'test'
11
+
12
+ Gem::Tasks.new(:build => {:tar => true, :zip => true}, :sign => {:checksum => true, :pgp => false})
13
+
14
+ namespace :rcov do
15
+ require 'rspec/core/rake_task'
16
+
17
+ RSpec::Core::RakeTask.new(:rspec)
18
+ end
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['RAILS_ENV'] = ENV['RAILS_ENV'] ? ENV['RAILS_ENV'] : 'production'
3
+
4
+ require 'syslogger'
5
+ require 'oneacct_exporter'
6
+ require 'oneacct_exporter/log'
7
+ require 'settings'
8
+ require 'fileutils'
9
+ require 'oneacct_opts'
10
+
11
+ options = OneacctOpts.parse(ARGV)
12
+
13
+ log = Logger.new(STDOUT)
14
+
15
+ if Settings['logging'] && Settings['logging']['log_type'] == 'file'
16
+ begin
17
+ log_file = File.open(Settings['logging']['log_file'], File::WRONLY | File::CREAT | File::APPEND)
18
+ log = Logger.new(log_file)
19
+ rescue => e
20
+ OneacctExporter::Log.setup_logging(log)
21
+ log.warn("Unable to create log file #{Settings['logging']['log_file']}: #{e.message}.\
22
+ Falling back to STDOUT.")
23
+ end
24
+ elsif Settings['logging'] && Settings['logging']['log_type'] == 'syslog'
25
+ log = Syslogger.new('oneacct-export')
26
+ end
27
+
28
+ OneacctExporter::Log.setup_log_level(log)
29
+
30
+ range = {}
31
+ range[:from] = options.records_from
32
+ range[:to] = options.records_to
33
+
34
+ groups = {}
35
+ groups[:include] = options.include_groups if options.include_groups
36
+ groups[:exclude] = options.exclude_groups if options.exclude_groups
37
+
38
+ if options.groups_file
39
+ log.debug('Reading groups from file...')
40
+ if File.exist?(options.groups_file) && File.readable?(options.groups_file)
41
+ file = File.open(options.groups_file, 'r')
42
+ file.each_line do |line|
43
+ groups[groups.keys.first] << line
44
+ end
45
+ file.close
46
+ else
47
+ log.error("File contaning groups: #{options.groups_file} doesn't exists or cannot be read. "\
48
+ 'Skipping groups restriction...')
49
+ groups[groups.keys.first] = []
50
+ end
51
+ end
52
+
53
+ begin
54
+ FileUtils.mkdir_p Settings.output['output_dir']
55
+ rescue SystemCallError => e
56
+ puts "Cannot create an output directory: #{e.message}. Quitting."
57
+ exit
58
+ end
59
+
60
+ log.debug('Creating OneacctExporter...')
61
+
62
+ opts = {}
63
+ opts[:range] = range
64
+ opts[:groups] = groups
65
+ opts[:blocking] = options.blocking
66
+ opts[:timeout] = options.timeout
67
+ opts[:compatibility] = options.compatibility
68
+
69
+ log.debug(opts)
70
+
71
+ oneacct_exporter = OneacctExporter.new(opts, log)
72
+ oneacct_exporter.export
data/config/conf.yml ADDED
@@ -0,0 +1,53 @@
1
+ #Five mandatory parameters:
2
+ # site_name
3
+ # cloud_type
4
+ # endpoint
5
+ # output -> output_dir
6
+ # output -> output_type
7
+ defaults: &defaults
8
+ site_name: <fill in>
9
+ cloud_type: <fill in>
10
+ endpoint: <fill in>
11
+ output:
12
+ output_dir: <fill in>
13
+ output_type: apel-0.2 #currently only option
14
+ # num_of_vms_per_file: 500
15
+ #
16
+ #logging:
17
+ # log_type: file #Two options: file, syslog. Defaults to stdout
18
+ # log_file: /var/log/oneacct-export.log #Used when type file selected
19
+ #
20
+ #xml_rpc:
21
+ # secret: username:password #If not specified looking for secret in ONE_AUTH and ~/.one/one_auth
22
+ # endpoint: http://localhost:2633/RPC2 #Defaults to content of ONE_XMLRPC or content of ~/.one/one_endpoint or http://localhost:2633/RPC2
23
+ #
24
+ #redis:
25
+ # namespace: oneacct_export
26
+ # url: redis://localhost:6379
27
+ # password: password
28
+ sidekiq:
29
+ queue: oneacct_export
30
+ production:
31
+ <<: *defaults
32
+ development:
33
+ <<: *defaults
34
+ test:
35
+ site_name: <placeholder>
36
+ cloud_type: <placeholder>
37
+ endpoint: <placeholder>
38
+ output:
39
+ output_dir: <placeholder>
40
+ output_type: <placeholder>
41
+ num_of_vms_per_file: <placeholder>
42
+ logging:
43
+ log_type: <placeholder>
44
+ log_file: <placeholder>
45
+ xml_rpc:
46
+ secret: <placeholder>
47
+ endpoint: <placeholder>
48
+ redis:
49
+ namespace: <placeholder>
50
+ url: http://some.random.url.com
51
+ password: password
52
+ sidekiq:
53
+ queue: oneacct_export
@@ -0,0 +1,6 @@
1
+ ---
2
+ :concurrency: 5
3
+ #:pidfile: ./tmp/pids/sidekiq.pid
4
+ #:logfile: ./log/sidekiq.log
5
+ :queues:
6
+ - oneacct_export
data/lib/errors.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'errors/authentication_error'
2
+ require 'errors/resource_not_found_error'
3
+ require 'errors/resource_retrieval_error'
4
+ require 'errors/resource_state_error'
5
+ require 'errors/user_not_authorized_error'
6
+
7
+ module Errors; end
@@ -0,0 +1,3 @@
1
+ module Errors
2
+ class AuthenticationError < ::StandardError; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module Errors
2
+ class ResourceNotFoundError < ::ArgumentError; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module Errors
2
+ class ResourceRetrievalError < ::StandardError; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module Errors
2
+ class ResourceStateError < ::StandardError; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module Errors
2
+ class UserNotAuthorizedError < AuthenticationError; end
3
+ end
@@ -0,0 +1,18 @@
1
+ require 'uri'
2
+
3
+ module InputValidator
4
+ URI_RE = /\A#{URI.regexp}\z/
5
+ NUMBER_RE = /\A[[:digit:]]+\z/
6
+
7
+ def is?(object, regexp)
8
+ object.to_s =~ regexp
9
+ end
10
+
11
+ def is_number?(object)
12
+ is?(object, NUMBER_RE)
13
+ end
14
+
15
+ def is_uri?(object)
16
+ is?(object, URI_RE)
17
+ end
18
+ end
@@ -0,0 +1,147 @@
1
+ require 'opennebula'
2
+ require 'settings'
3
+ require 'errors'
4
+ require 'logger'
5
+ require 'input_validator'
6
+
7
+ class OneDataAccessor
8
+ include Errors
9
+ include InputValidator
10
+
11
+ STATE_DONE = '6'
12
+
13
+ attr_reader :log, :batch_size, :client, :compatibility
14
+
15
+ def initialize(compatibility, log = nil)
16
+ @log = log ? log : Logger.new(STDOUT)
17
+ @compatibility = compatibility
18
+
19
+ @batch_size = Settings.output['num_of_vms_per_file'] ? Settings.output['num_of_vms_per_file'] : 500
20
+ fail ArgumentError, 'Wrong number of vms per file.' unless is_number?(@batch_size)
21
+
22
+ @compatibility_vm_pool = nil
23
+
24
+ initialize_client
25
+ end
26
+
27
+ def initialize_client
28
+ secret = Settings['xml_rpc'] ? Settings.xml_rpc['secret'] : nil
29
+ endpoint = Settings['xml_rpc'] ? Settings.xml_rpc['endpoint'] : nil
30
+ fail ArgumentError, "#{endpoint} is not a valid URL." if endpoint && !is_uri?(endpoint)
31
+
32
+ @client = OpenNebula::Client.new(secret, endpoint)
33
+ end
34
+
35
+ def mapping(pool_class, xpath)
36
+ @log.debug("Generating mapping for class: #{pool_class} and xpath: '#{xpath}'.")
37
+ pool = pool_class.new(@client)
38
+ if pool.respond_to? 'info_all'
39
+ rc = pool.info_all
40
+ check_retval(rc, Errors::ResourceRetrievalError)
41
+ else
42
+ rc = pool.info
43
+ check_retval(rc, Errors::ResourceRetrievalError)
44
+ end
45
+
46
+ map = {}
47
+ pool.each do |item|
48
+ unless item['ID']
49
+ @log.error("Skipping a resource of the type #{pool_class} without an ID present.")
50
+ next
51
+ end
52
+ map[item['ID']] = item[xpath]
53
+ end
54
+
55
+ map
56
+ end
57
+
58
+ def vm(vm_id)
59
+ fail ArgumentError, "#{vm_id} is not a valid id." unless is_number?(vm_id)
60
+ @log.debug("Retrieving virtual machine with id: #{vm_id}.")
61
+ vm = OpenNebula::VirtualMachine.new(OpenNebula::VirtualMachine.build_xml(vm_id), @client)
62
+ rc = vm.info
63
+ check_retval(rc, Errors::ResourceRetrievalError)
64
+ vm
65
+ end
66
+
67
+ def vms(batch_number, range, groups)
68
+ vms = []
69
+ vm_pool = load_vm_pool(batch_number)
70
+ return nil if vm_pool.count == 0
71
+
72
+ @log.debug("Searching for vms based on range: #{range} and groups: #{groups}.")
73
+ vm_pool.each do |vm|
74
+ unless vm['ID']
75
+ @log.error('Skipping a record without an ID present.')
76
+ next
77
+ end
78
+
79
+ next unless want?(vm, range, groups)
80
+
81
+ vms << vm['ID'].to_i
82
+ end
83
+
84
+ @log.debug("Selected vms: #{vms}.")
85
+ vms
86
+ end
87
+
88
+ def want?(vm, range, groups)
89
+ if vm.nil?
90
+ @log.warn('Obtained nil vm from vm pool.')
91
+ return false
92
+ end
93
+ # range restriction
94
+ unless range.nil? || range.empty?
95
+ return false if range[:from] && vm['STATE'] == STATE_DONE && vm['ETIME'].to_i < range[:from].to_i
96
+ return false if range[:to] && vm['STIME'].to_i > range[:to].to_i
97
+ end
98
+
99
+ # groups restriction
100
+ unless groups.nil? || groups.empty?
101
+ return false if groups[:include] && !groups[:include].include?(vm['GNAME'])
102
+ return false if groups[:exclude] && groups[:exclude].include?(vm['GNAME'])
103
+ end
104
+
105
+ true
106
+ end
107
+
108
+ def load_vm_pool(batch_number)
109
+ fail ArgumentError, "#{batch_number} is not a valid number" unless is_number?(batch_number)
110
+ @log.debug("Loading vm pool with batch number: #{batch_number}.")
111
+ from = batch_number * @batch_size
112
+ to = (batch_number + 1) * @batch_size - 1
113
+
114
+ if @compatibility
115
+ unless @compatibility_vm_pool
116
+ vm_pool = OpenNebula::VirtualMachinePool.new(@client)
117
+ rc = vm_pool.info(OpenNebula::Pool::INFO_ALL, -1, -1, OpenNebula::VirtualMachinePool::INFO_ALL_VM)
118
+ check_retval(rc, Errors::ResourceRetrievalError)
119
+ @compatibility_vm_pool = vm_pool.to_a
120
+ end
121
+
122
+ return @compatibility_vm_pool[from..to] || []
123
+ else
124
+ vm_pool = OpenNebula::VirtualMachinePool.new(@client)
125
+ rc = vm_pool.info(OpenNebula::Pool::INFO_ALL, from, to, OpenNebula::VirtualMachinePool::INFO_ALL_VM)
126
+ check_retval(rc, Errors::ResourceRetrievalError)
127
+
128
+ return vm_pool
129
+ end
130
+ end
131
+
132
+ def check_retval(rc, e_klass)
133
+ return true unless OpenNebula.is_error?(rc)
134
+ case rc.errno
135
+ when OpenNebula::Error::EAUTHENTICATION
136
+ fail Errors::AuthenticationError, rc.message
137
+ when OpenNebula::Error::EAUTHORIZATION
138
+ fail Errors::UserNotAuthorizedError, rc.message
139
+ when OpenNebula::Error::ENO_EXISTS
140
+ fail Errors::ResourceNotFoundError, rc.message
141
+ when OpenNebula::Error::EACTION
142
+ fail Errors::ResourceStateError, rc.message
143
+ else
144
+ fail e_klass, rc.message
145
+ end
146
+ end
147
+ end
data/lib/one_worker.rb ADDED
@@ -0,0 +1,181 @@
1
+ file_dir_name = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(file_dir_name) unless $LOAD_PATH.include?(file_dir_name)
3
+
4
+ require 'sidekiq'
5
+ require 'one_data_accessor'
6
+ require 'one_writer'
7
+ require 'sidekiq_conf'
8
+ require 'oneacct_exporter/log'
9
+ require 'settings'
10
+
11
+ class OneWorker
12
+ include Sidekiq::Worker
13
+
14
+ sidekiq_options retry: 5, dead: false,\
15
+ queue: (Settings['sidekiq'] && Settings.sidekiq['queue']) ? Settings.sidekiq['queue'].to_sym : :default
16
+
17
+ B_IN_GB = 1_073_741_824
18
+
19
+ STRING = /[[:print:]]+/
20
+ NUMBER = /[[:digit:]]+/
21
+ NON_ZERO = /[1-9][[:digit:]]*/
22
+ STATES = %w(started started suspended started suspended suspended completed completed suspended)
23
+
24
+ def common_data
25
+ common_data = {}
26
+ common_data['endpoint'] = Settings['endpoint']
27
+ common_data['site_name'] = Settings['site_name']
28
+ common_data['cloud_type'] = Settings['cloud_type']
29
+
30
+ common_data
31
+ end
32
+
33
+ def create_user_map(oda)
34
+ logger.debug('Creating user map.')
35
+ create_map(OpenNebula::UserPool, 'TEMPLATE/X509_DN', oda)
36
+ end
37
+
38
+ def create_image_map(oda)
39
+ logger.debug('Creating image map.')
40
+ create_map(OpenNebula::ImagePool, 'NAME', oda)
41
+ end
42
+
43
+ def create_map(pool_type, mapping, oda)
44
+ oda.mapping(pool_type, mapping)
45
+ rescue => e
46
+ msg = "Couldn't create map: #{e.message}. "\
47
+ 'Stopping to avoid malformed records.'
48
+ logger.error(msg)
49
+ raise msg
50
+ end
51
+
52
+ def load_vm(vm_id, oda)
53
+ oda.vm(vm_id)
54
+ rescue => e
55
+ logger.error("Couldn't retrieve data for vm with id: #{vm_id}. #{e.message}. Skipping.")
56
+ return nil
57
+ end
58
+
59
+ def process_vm(vm, user_map, image_map)
60
+ data = common_data.clone
61
+
62
+ data['vm_uuid'] = parse(vm['ID'], STRING)
63
+ unless vm['STIME']
64
+ logger.error('Skipping a malformed record. '\
65
+ "VM with id #{data['vm_uuid']} has no StartTime.")
66
+ return nil
67
+ end
68
+
69
+ data['start_time'] = parse(vm['STIME'], NUMBER)
70
+ start_time = data['start_time'].to_i
71
+ if start_time == 0
72
+ logger.error('Skipping a malformed record. '\
73
+ "VM with id #{data['vm_uuid']} has malformed StartTime.")
74
+ return nil
75
+ end
76
+ data['start_time_readable'] = parse(Time.at(start_time).strftime('%F %T%:z'), STRING)
77
+ data['end_time'] = parse(vm['ETIME'], NON_ZERO)
78
+ end_time = data['end_time'].to_i
79
+
80
+ if end_time != 0 && start_time > end_time
81
+ logger.error('Skipping malformed record. '\
82
+ "VM with id #{data['vm_uuid']} has wrong time entries.")
83
+ return nil
84
+ end
85
+
86
+ data['machine_name'] = parse(vm['DEPLOY_ID'], STRING, "one-#{data['vm_uuid']}")
87
+ data['user_id'] = parse(vm['UID'], STRING)
88
+ data['group_id'] = parse(vm['GID'], STRING)
89
+ data['user_name'] = parse(user_map[data['user_id']], STRING)
90
+ data['fqan'] = parse(vm['GNAME'], STRING, nil)
91
+
92
+ if vm['STATE']
93
+ data['status'] = parse(STATES[vm['STATE'].to_i], STRING)
94
+ else
95
+ data['status'] = 'NULL'
96
+ end
97
+
98
+ unless vm['HISTORY_RECORDS/HISTORY[1]']
99
+ logger.warn('Skipping malformed record. '\
100
+ "VM with id #{data['vm_uuid']} has no history records.")
101
+ return nil
102
+ end
103
+
104
+ rstime = sum_rstime(vm)
105
+ return nil unless rstime
106
+
107
+ data['duration'] = parse(rstime.to_s, NON_ZERO)
108
+
109
+ suspend = (end_time - start_time) - data['duration'].to_i unless end_time == 0
110
+ data['suspend'] = parse(suspend.to_s, NUMBER)
111
+
112
+ vcpu = vm['TEMPLATE/VCPU']
113
+ data['cpu_count'] = parse(vcpu, NON_ZERO, '1')
114
+
115
+ net_tx = parse(vm['NET_TX'], NUMBER, 0)
116
+ data['network_inbound'] = (net_tx.to_i / B_IN_GB).round
117
+ net_rx = parse(vm['NET_RX'], NUMBER, 0)
118
+ data['network_outbound'] = (net_rx.to_i / B_IN_GB).round
119
+
120
+ data['memory'] = parse(vm['MEMORY'], NUMBER, '0')
121
+ data['image_name'] = parse(image_map[vm['TEMPLATE/DISK[1]/IMAGE_ID']], STRING)
122
+
123
+ data
124
+ end
125
+
126
+ def sum_rstime(vm)
127
+ rstime = 0
128
+ vm.each 'HISTORY_RECORDS/HISTORY' do |h|
129
+ next unless h['RSTIME'] && h['RETIME'] && h['RETIME'] != '0' && h['RSTIME'] != '0'
130
+ if h['RSTIME'].to_i > h['RETIME'].to_i
131
+ logger.warn('Skipping malformed record. '\
132
+ "VM with id #{vm['ID']} has wrong CpuDuration.")
133
+ rstime = nil
134
+ break
135
+ end
136
+ rstime += h['RETIME'].to_i - h['RSTIME'].to_i
137
+ end
138
+
139
+ rstime
140
+ end
141
+
142
+ def perform(vms, output)
143
+ OneacctExporter::Log.setup_log_level(logger)
144
+
145
+ vms = vms.split('|')
146
+
147
+ oda = OneDataAccessor.new(logger)
148
+ user_map = create_user_map(oda)
149
+ image_map = create_image_map(oda)
150
+
151
+ data = []
152
+
153
+ vms.each do |vm_id|
154
+ vm = load_vm(vm_id, oda)
155
+ next unless vm
156
+
157
+ logger.debug("Processing vm with id: #{vm_id}.")
158
+ vm_data = process_vm(vm, user_map, image_map)
159
+ next unless vm_data
160
+
161
+ logger.debug("Adding vm with data: #{vm_data} for export.")
162
+ data << vm_data
163
+ end
164
+
165
+ write_data(data, output)
166
+ end
167
+
168
+ def write_data(data, output)
169
+ logger.debug('Creating writer...')
170
+ ow = OneWriter.new(data, output, logger)
171
+ ow.write
172
+ rescue => e
173
+ msg = "Canno't write result to #{output}: #{e.message}"
174
+ logger.error(msg)
175
+ raise msg
176
+ end
177
+
178
+ def parse(value, regex, substitute = 'NULL')
179
+ regex =~ value ? value : substitute
180
+ end
181
+ end