opennebula-cli 5.10.1 → 5.10.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e9e19951cf0b76618715d35823cad93f8d5b6d87
4
- data.tar.gz: 3d6f919274af571c80ad6f229c7c66829b09e6ea
3
+ metadata.gz: 295cccef022aa2264f24ed89bdd3c7ff0a88d7af
4
+ data.tar.gz: d0147da36b571179d0a783eb4a0aca095eb75170
5
5
  SHA512:
6
- metadata.gz: 105ad944601a3b77a4b6f4ded271b4fc2eb6df94ea130224db117b47f17a9d8a5e19db6bbafd98144cd3e1b1ccff6cad357f6c35e7ce1026e406e895e6a6ff67
7
- data.tar.gz: d76e4978c73057f0ec6b39960d16f5b9bd4b7be46525847dffb8664441ea44cc12c266cba56374da9138c36c6d41211a51f412ba495b4b17114be149c49a0ae4
6
+ metadata.gz: a09dc31b0b49468074f6ec70bdebf46e9f59f174226c328fe77b62add87a1b96c4620705e432271d9775875e529e92079fb19571bc142fa7aa55602e02d9711b
7
+ data.tar.gz: 4632563ed7e4098f7a20e48e30f6842574664ee0b3ab37ba2b3819a03294b07770fe54cd1247805d67429d375bd2247f2f611ef850da71076779880b6eb4684e
data/bin/onevcenter CHANGED
@@ -286,4 +286,49 @@ CommandParser::CmdParser.new(ARGV) do
286
286
 
287
287
  exit 0
288
288
  end
289
+
290
+ ############################################################################
291
+ # Clear VM tags
292
+ ############################################################################
293
+ cleartags_desc = <<-EOT.unindent
294
+ Clear extraconfig tags from a vCenter VM, useful when a VM has been
295
+ launched by OpenNebula and needs to be reimported
296
+
297
+ Example:
298
+ - Clean VM 15:
299
+
300
+ onevcenter cleargs 15
301
+ EOT
302
+ command :cleartags, cleartags_desc, :vmid do
303
+ vmid = args[0]
304
+ remove_str = "\n onevm recover --delete-db #{vmid}" \
305
+ "\n\nAfter a monitoring cycle, the VM will appear "\
306
+ 'as a Wild VM for reimport.'
307
+
308
+ print 'Extracting information from VM ' + vmid
309
+
310
+ begin
311
+ print '.'
312
+ vm, keys = helper.clear_tags(vmid)
313
+ print '.'
314
+
315
+ if keys.empty?
316
+ puts "\n\nNo OpenNebula keys present, is safe to remove the VM."
317
+ puts remove_str
318
+ exit 0
319
+ end
320
+ puts '.'
321
+
322
+ puts 'The following keys will be removed:'
323
+ keys.each {|key| puts "\t- #{key}" }
324
+
325
+ helper.remove_keys(vm, keys)
326
+ rescue StandardError => e
327
+ STDERR.puts "Couldn't clear VM tags. Reason: #{e.message}"
328
+ exit 1
329
+ end
330
+ puts "\nKeys removed from VM. Is safe to remove it"
331
+ puts remove_str
332
+ exit 0
333
+ end
289
334
  end
data/bin/onezone CHANGED
@@ -51,6 +51,12 @@ CommandParser::CmdParser.new(ARGV) do
51
51
  :format => String
52
52
  }
53
53
 
54
+ DATABASE = {
55
+ :name => 'database',
56
+ :large => '--db',
57
+ :description => 'Also sync database'
58
+ }
59
+
54
60
  before_proc do
55
61
  helper.set_client(options)
56
62
  end
@@ -215,4 +221,33 @@ CommandParser::CmdParser.new(ARGV) do
215
221
  helper.set_zone(args[0], false)
216
222
  end
217
223
  end
224
+
225
+ sync_desc = <<-EOT.unindent
226
+ Syncs configuration files and folders from another server
227
+
228
+ This command must be executed under root
229
+ EOT
230
+
231
+ command :serversync, sync_desc, :server, :options => [DATABASE] do
232
+ begin
233
+ gem 'augeas', '~> 0.6'
234
+ require 'augeas'
235
+ rescue Gem::LoadError
236
+ STDERR.puts(
237
+ 'Augeas gem is not installed, run `gem install ' \
238
+ 'augeas -v \'0.6\'` to install it'
239
+ )
240
+ exit(-1)
241
+ end
242
+
243
+ if !Process.uid.zero? || !Process.gid.zero?
244
+ STDERR.puts("'onezone serversync' must be run under root")
245
+ exit(-1)
246
+ end
247
+
248
+ server = Replicator.new('/var/lib/one/.ssh/id_rsa', args[0])
249
+ server.process_files(options.key?(:database))
250
+
251
+ 0
252
+ end
218
253
  end
@@ -183,6 +183,20 @@ EOT
183
183
  answer = Base64::encode64(answer).strip.delete("\n")
184
184
  end
185
185
 
186
+ when 'boolean'
187
+ print header
188
+
189
+ answer = STDIN.readline.chop
190
+
191
+ # use default in case it's empty
192
+ answer = initial if answer.empty?
193
+
194
+ unless %w[YES NO].include?(answer)
195
+ STDERR.puts "Invalid boolean '#{answer}'"
196
+ STDERR.puts 'Boolean has to be YES or NO'
197
+ exit(-1)
198
+ end
199
+
186
200
  when 'password'
187
201
  print header
188
202
 
@@ -425,4 +425,59 @@ class OneVcenterHelper < OpenNebulaHelper::OneHelper
425
425
 
426
426
  return opts
427
427
  end
428
+
429
+ def clear_tags(vmid)
430
+ client = Client.new
431
+
432
+ vm_pool = VirtualMachinePool.new(client, -1)
433
+ host_pool = HostPool.new(client)
434
+ deploy_id = -1
435
+ host_id = -1
436
+ hostname = ''
437
+
438
+ rc = vm_pool.info
439
+ raise rc.message if OpenNebula.is_error?(rc)
440
+
441
+ rc = host_pool.info
442
+ raise rc.message if OpenNebula.is_error?(rc)
443
+
444
+ vm_pool.each do |vm|
445
+ next if vm.id.to_s != vmid
446
+
447
+ deploy_id = vm.deploy_id
448
+ vm_history = vm.to_hash['VM']['HISTORY_RECORDS']['HISTORY']
449
+ hostname = vm_history['HOSTNAME']
450
+ break
451
+ end
452
+
453
+ host_pool.each do |host|
454
+ if host.name == hostname
455
+ host_id = host.id
456
+ end
457
+ end
458
+
459
+ vi_client = VCenterDriver::VIClient.new_from_host(host_id)
460
+ vm = VCenterDriver::VirtualMachine
461
+ .new(vi_client, deploy_id, vmid)
462
+
463
+ keys_to_remove = []
464
+ vm['config.extraConfig'].each do |extraconfig|
465
+ if extraconfig.key.include?('opennebula.disk') ||
466
+ extraconfig.key.include?('opennebula.vm')
467
+ keys_to_remove << extraconfig.key
468
+ end
469
+ end
470
+
471
+ return vm, keys_to_remove
472
+ end
473
+
474
+ def remove_keys(vm, keys_to_remove)
475
+ spec_hash = keys_to_remove.map {|key| { :key => key, :value => '' } }
476
+
477
+ spec = RbVmomi::VIM.VirtualMachineConfigSpec(
478
+ :extraConfig => spec_hash
479
+ )
480
+ vm.item.ReconfigVM_Task(:spec => spec).wait_for_completion
481
+ end
482
+
428
483
  end
@@ -14,8 +14,434 @@
14
14
  # limitations under the License. #
15
15
  #--------------------------------------------------------------------------- #
16
16
 
17
+ require 'fileutils'
18
+ require 'tempfile'
19
+ require 'CommandManager'
20
+
17
21
  require 'one_helper'
18
22
 
23
+ # Check differences between files and copy them
24
+ class Replicator
25
+
26
+ SSH_OPTIONS = '-o stricthostkeychecking=no -o passwordauthentication=no'
27
+ ONE_AUTH = '/var/lib/one/.one/one_auth'
28
+ FED_ATTRS = %w[MODE ZONE_ID SERVER_ID MASTER_ONED]
29
+
30
+ FILES = [
31
+ { :name => 'az_driver.conf',
32
+ :service => 'opennebula' },
33
+ { :name => 'az_driver.default',
34
+ :service => 'opennebula' },
35
+ { :name => 'ec2_driver.conf',
36
+ :service => 'opennebula' },
37
+ { :name => 'ec2_driver.default',
38
+ :service => 'opennebula' },
39
+ { :name => 'econe.conf',
40
+ :service => 'opennebula-econe' },
41
+ { :name => 'oneflow-server.conf',
42
+ :service => 'opennebula-flow' },
43
+ { :name => 'onegate-server.conf',
44
+ :service => 'opennebula-gate' },
45
+ { :name => 'sched.conf',
46
+ :service => 'opennebula' },
47
+ { :name => 'sunstone-logos.yaml',
48
+ :service => 'opennebula-sunstone' },
49
+ { :name => 'sunstone-server.conf',
50
+ :service => 'opennebula-sunstone' },
51
+ { :name => 'vcenter_driver.default',
52
+ :service => 'opennebula' }
53
+ ]
54
+
55
+ FOLDERS = [
56
+ { :name => 'sunstone-views', :service => 'opennebula-sunstone' },
57
+ { :name => 'auth', :service => 'opennebula' },
58
+ { :name => 'ec2query_templates', :service => 'opennebula' },
59
+ { :name => 'hm', :service => 'opennebula' },
60
+ { :name => 'sunstone-views', :service => 'opennebula' },
61
+ { :name => 'vmm_exec', :service => 'opennebula' }
62
+ ]
63
+
64
+ # Class constructor
65
+ #
66
+ # @param ssh_key [String] SSH key file path
67
+ # @param server [String] OpenNebula server IP address
68
+ def initialize(ssh_key, server)
69
+ @oneadmin_identity_file = ssh_key
70
+ @remote_server = server
71
+
72
+ # Get local configuration
73
+ l_credentials = File.read(ONE_AUTH).gsub("\n", '')
74
+ l_endpoint = 'http://localhost:2633/RPC2'
75
+ local_client = Client.new(l_credentials, l_endpoint)
76
+
77
+ @l_config = OpenNebula::System.new(local_client).get_configuration
78
+ @l_config_elements = { :raw => @l_config }
79
+ @l_fed_elements = { :raw => @l_config }
80
+
81
+ fetch_db_config(@l_config_elements)
82
+ fetch_fed_config(@l_fed_elements)
83
+
84
+ # Get remote configuration
85
+ r_credentials = ssh("cat #{ONE_AUTH}").stdout.gsub("\n", '')
86
+ r_endpoint = "http://#{server}:2633/RPC2"
87
+ remote_client = Client.new(r_credentials, r_endpoint)
88
+
89
+ @r_config = OpenNebula::System.new(remote_client).get_configuration
90
+ @r_config_elements = { :raw => @r_config }
91
+ @r_fed_elements = { :raw => @r_config }
92
+
93
+ fetch_db_config(@r_config_elements)
94
+ fetch_fed_config(@r_fed_elements)
95
+
96
+ # Set OpenNebula services to not restart
97
+ @opennebula_services = { 'opennebula' => false,
98
+ 'opennebula-sunstone' => false,
99
+ 'opennebula-gate' => false,
100
+ 'opennebula-flow' => false,
101
+ 'opennebula-econe' => false }
102
+ end
103
+
104
+ # Process files and folders
105
+ #
106
+ # @param sync_db [Boolean] True to sync database
107
+ def process_files(sync_db)
108
+ # Files to be copied
109
+ copy_onedconf
110
+
111
+ FILES.each do |file|
112
+ copy_and_check(file[:name], file[:service])
113
+ end
114
+
115
+ # Folders to be copied
116
+ FOLDERS.each do |folder|
117
+ copy_folder(folder[:name], folder[:service])
118
+ end
119
+
120
+ restart_services
121
+
122
+ # Sync database
123
+ sync_db
124
+ end
125
+
126
+ private
127
+
128
+ # Get database configuration
129
+ #
130
+ # @param configs [Object] Configuration
131
+ def fetch_db_config(configs)
132
+ configs.store(:backend, configs[:raw]['/TEMPLATE/DB/BACKEND'])
133
+
134
+ if configs[:backend] == 'mysql'
135
+ configs.store(:server, configs[:raw]['/TEMPLATE/DB/SERVER'])
136
+ configs.store(:user, configs[:raw]['/TEMPLATE/DB/USER'])
137
+ configs.store(:password, configs[:raw]['/TEMPLATE/DB/PASSWD'])
138
+ configs.store(:dbname, configs[:raw]['/TEMPLATE/DB/DB_NAME'])
139
+ configs.store(:port, configs[:raw]['/TEMPLATE/DB/PORT'])
140
+ configs[:port] = '3306' if configs[:port] == '0'
141
+ else
142
+ STDERR.puts 'No mysql backend configuration found'
143
+ exit(-1)
144
+ end
145
+ end
146
+
147
+ # Get federation configuration
148
+ #
149
+ # @param configs [Object] Configuration
150
+ def fetch_fed_config(configs)
151
+ configs.store(:server_id,
152
+ configs[:raw]['/TEMPLATE/FEDERATION/SERVER_ID'])
153
+ configs.store(:zone_id,
154
+ configs[:raw]['/TEMPLATE/FEDERATION/ZONE_ID'])
155
+ end
156
+
157
+ # Replaces a file with the version located on a remote server
158
+ # Only replaces the file if it's different from the remote one
159
+ #
160
+ # @param file [String] File to check
161
+ # @param service [String] Service to restart
162
+ def copy_and_check(file, service)
163
+ puts "Checking #{file}"
164
+
165
+ temp_file = Tempfile.new("#{file}-temp")
166
+
167
+ scp("/etc/one/#{file}", temp_file.path)
168
+
169
+ if !FileUtils.compare_file(temp_file, "/etc/one/#{file}")
170
+ FileUtils.cp(temp_file.path, "/etc/one/#{file}")
171
+
172
+ puts "#{file} has been replaced by #{@remote_server}:#{file}"
173
+
174
+ @opennebula_services[service] = true
175
+ end
176
+ ensure
177
+ temp_file.unlink
178
+ end
179
+
180
+ # Copy folders
181
+ #
182
+ # @param folder [String] Folder to copy
183
+ # @param service [String] Service to restart
184
+ def copy_folder(folder, service)
185
+ puts "Checking #{folder}"
186
+
187
+ rc = run_command(
188
+ "rsync -ai\
189
+ -e \"ssh #{SSH_OPTIONS} -i #{@oneadmin_identity_file}\" " \
190
+ "#{@remote_server}:/etc/one/#{folder}/ " \
191
+ "/etc/one/#{folder}/"
192
+ )
193
+
194
+ unless rc
195
+ rc = run_command(
196
+ "rsync -ai\
197
+ -e \"ssh #{SSH_OPTIONS} -i #{@oneadmin_identity_file}\" " \
198
+ "oneadmin@#{@remote_server}:/etc/one/#{folder}/ " \
199
+ "/etc/one/#{folder}/"
200
+ )
201
+ end
202
+
203
+ unless rc
204
+ STDERR.puts 'ERROR'
205
+ STDERR.puts "Fail to sync #{folder}"
206
+ exit(-1)
207
+ end
208
+
209
+ output = rc.stdout
210
+
211
+ return if output.empty?
212
+
213
+ puts "Folder #{folder} has been sync with #{@remote_server}:#{folder}"
214
+
215
+ @opennebula_services[service] = true
216
+ end
217
+
218
+ # oned.conf file on distributed environments will always be different,
219
+ # due to the federation section.
220
+ # Replace oned.conf based on a remote server's version maintaining
221
+ # the old FEDERATION section
222
+ def copy_onedconf
223
+ puts 'Checking oned.conf'
224
+
225
+ # Create temporarhy files
226
+ l_oned = Tempfile.new('l_oned')
227
+ r_oned = Tempfile.new('r_oned')
228
+
229
+ l_oned.close
230
+ r_oned.close
231
+
232
+ # Copy remote and local oned.conf files to temporary files
233
+ scp('/etc/one/oned.conf', r_oned.path)
234
+
235
+ FileUtils.cp('/etc/one/oned.conf', l_oned.path)
236
+
237
+ # Create augeas objects to manage oned.conf files
238
+ l_work_file_dir = File.dirname(l_oned.path)
239
+ l_work_file_name = File.basename(l_oned.path)
240
+
241
+ r_work_file_dir = File.dirname(r_oned.path)
242
+ r_work_file_name = File.basename(r_oned.path)
243
+
244
+ l_aug = Augeas.create(:no_modl_autoload => true,
245
+ :no_load => true,
246
+ :root => l_work_file_dir,
247
+ :loadpath => l_oned.path)
248
+
249
+ l_aug.clear_transforms
250
+ l_aug.transform(:lens => 'Oned.lns', :incl => l_work_file_name)
251
+ l_aug.context = "/files/#{l_work_file_name}"
252
+ l_aug.load
253
+
254
+ r_aug = Augeas.create(:no_modl_autoload => true,
255
+ :no_load => true,
256
+ :root => r_work_file_dir,
257
+ :loadpath => r_oned.path)
258
+
259
+ r_aug.clear_transforms
260
+ r_aug.transform(:lens => 'Oned.lns', :incl => r_work_file_name)
261
+ r_aug.context = "/files/#{r_work_file_name}"
262
+ r_aug.load
263
+
264
+ # Get local federation information
265
+ fed_attrs = []
266
+
267
+ FED_ATTRS.each do |attr|
268
+ fed_attrs << l_aug.get("FEDERATION/#{attr}")
269
+ end
270
+
271
+ # Remove federation section
272
+ l_aug.rm('FEDERATION')
273
+ r_aug.rm('FEDERATION')
274
+
275
+ # Save augeas files in temporary directories
276
+ l_aug.save
277
+ r_aug.save
278
+
279
+ return if FileUtils.compare_file(l_oned.path, r_oned.path)
280
+
281
+ time_based_identifier = Time.now.to_i
282
+
283
+ # backup oned.conf
284
+ FileUtils.cp('/etc/one/oned.conf',
285
+ "/etc/one/oned.conf#{time_based_identifier}")
286
+
287
+ FED_ATTRS.zip(fed_attrs) do |name, value|
288
+ r_aug.set("FEDERATION/#{name}", value)
289
+ end
290
+
291
+ r_aug.save
292
+
293
+ FileUtils.cp(r_oned.path, '/etc/one/oned.conf')
294
+
295
+ puts 'oned.conf has been replaced by ' \
296
+ "#{@remote_server}:/etc/one/oned.conf"
297
+
298
+ puts 'A copy of your old oned.conf file is located here: ' \
299
+ "/etc/one/oned.conf#{time_based_identifier}"
300
+
301
+ @opennebula_services['opennebula'] = true
302
+ end
303
+
304
+ # Sync database
305
+ def sync_db
306
+ puts "Dumping and fetching database from #{@remote_server}, " \
307
+ 'this could take a while'
308
+
309
+ ssh(
310
+ "onedb backup -f -u #{@r_config_elements[:user]} " \
311
+ "-p #{@r_config_elements[:password]} " \
312
+ "-d #{@r_config_elements[:dbname]} " \
313
+ "-P #{@r_config_elements[:port]} /tmp/one_db_dump.sql"
314
+ )
315
+
316
+ scp('/tmp/one_db_dump.sql', '/tmp/one_db_dump.sql')
317
+
318
+ puts "Local OpenNebula's database will be replaced, hang tight"
319
+
320
+ service_action('opennebula', 'stop')
321
+
322
+ puts 'Restoring database'
323
+
324
+ run_command(
325
+ "onedb restore -f -u #{@l_config_elements[:user]} " \
326
+ "-p #{@l_config_elements[:password]} " \
327
+ "-d #{@l_config_elements[:dbname]} " \
328
+ "-P #{@l_config_elements[:port]} " \
329
+ "-h #{@l_config_elements[:server]}" \
330
+ '/tmp/one_db_dump.sql',
331
+ true
332
+ )
333
+
334
+ service_action('opennebula', 'start')
335
+ end
336
+
337
+ # Run local command
338
+ #
339
+ # @param cmd [String] Command to run
340
+ # @param print_output [Boolean] True to show output
341
+ def run_command(cmd, print_output = false)
342
+ output = LocalCommand.run(cmd)
343
+
344
+ if output.code == 0
345
+ output
346
+ else
347
+ return false unless print_output
348
+
349
+ STDERR.puts 'ERROR'
350
+ STDERR.puts "Failed to run: #{cmd}"
351
+ STDERR.puts output.stderr
352
+
353
+ false
354
+ end
355
+ end
356
+
357
+ # Execute SSH command
358
+ #
359
+ # @param cmd [String] Command to execute
360
+ def ssh(cmd)
361
+ rc = run_command(
362
+ "ssh -i #{@oneadmin_identity_file} " \
363
+ "#{SSH_OPTIONS} #{@remote_server} " \
364
+ "#{cmd}"
365
+ )
366
+
367
+ # if default users doesn't work, try with oneadmin
368
+ unless rc
369
+ rc = run_command(
370
+ "ssh -i #{@oneadmin_identity_file} " \
371
+ "#{SSH_OPTIONS} oneadmin@#{@remote_server} " \
372
+ "#{cmd}"
373
+ )
374
+ end
375
+
376
+ # if oneadmin doesn't work neither, fail
377
+ unless rc
378
+ STDERR.puts 'ERROR'
379
+ STDERR.puts "Couldn't execute command #{cmd} on remote host"
380
+ exit(-1)
381
+ end
382
+
383
+ rc
384
+ end
385
+
386
+ # Execute SCP command
387
+ #
388
+ # @param src [String] Source path
389
+ # @param dest [String] Destination path
390
+ def scp(src, dest)
391
+ rc = run_command(
392
+ "scp -i #{@oneadmin_identity_file} " \
393
+ "#{SSH_OPTIONS} #{@remote_server}:/#{src} #{dest}"
394
+ )
395
+
396
+ # if default users doesn't work, try with oneadmin
397
+ unless rc
398
+ rc = run_command(
399
+ "scp -i #{@oneadmin_identity_file} " \
400
+ "#{SSH_OPTIONS} oneadmin@#{@remote_server}:#{src} #{dest}"
401
+ )
402
+ end
403
+
404
+ # if oneadmin doesn't work neither, fail
405
+ unless rc
406
+ STDERR.puts 'ERROR'
407
+ STDERR.puts "Couldn't execute command #{cmd} on remote host"
408
+ exit(-1)
409
+ end
410
+ end
411
+
412
+ # Restart OpenNebula services
413
+ def restart_services
414
+ restarted = false
415
+
416
+ @opennebula_services.each do |service, status|
417
+ next unless status
418
+
419
+ service_action(service)
420
+
421
+ restarted = true
422
+ end
423
+
424
+ return if restarted
425
+
426
+ puts 'Everything seems synchronized, nothing was replaced.'
427
+ end
428
+
429
+ # Service action
430
+ #
431
+ # @param service [String] Service to restart
432
+ # @param action [String] Action to execute (start, stop, restart)
433
+ def service_action(service, action = 'try-restart')
434
+ if `file /sbin/init`.include? 'systemd'
435
+ puts "#{action}ing #{service} via systemd"
436
+ run_command("systemctl #{action} #{service}", true)
437
+ else
438
+ puts "#{action}ing #{service} via init"
439
+ run_command("service #{service} #{action}", true)
440
+ end
441
+ end
442
+
443
+ end
444
+
19
445
  class OneZoneHelper < OpenNebulaHelper::OneHelper
20
446
 
21
447
  SERVER_NAME={
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opennebula-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.10.1
4
+ version: 5.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenNebula
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-10 00:00:00.000000000 Z
11
+ date: 2020-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opennebula
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 5.10.1
19
+ version: 5.10.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 5.10.1
26
+ version: 5.10.2
27
27
  description: Commands used to talk to OpenNebula
28
28
  email: contact@opennebula.org
29
29
  executables: