opennebula-cli 5.10.1 → 5.10.2

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.
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: