oneacct-export 0.1.0

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