Project

General

Profile

Download (23.5 KB) Statistics
| Branch: | Tag: | Revision:

hammer-cli-csv / lib / hammer_cli_csv / base.rb @ bfc065ce

1
# Copyright (c) 2013-2014 Red Hat
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.
23
#
24
#
25

    
26
require 'hammer_cli'
27
require 'katello_api'
28
require 'foreman_api'
29
require 'json'
30
require 'csv'
31

    
32
module HammerCLICsv
33
  class BaseCommand < HammerCLI::AbstractCommand
34

    
35
    NAME = 'Name'
36
    COUNT = 'Count'
37

    
38
    option ["-v", "--verbose"], :flag, "be verbose"
39
    option ['--threads'], 'THREAD_COUNT', 'Number of threads to hammer with', :default => 1
40
    option ['--csv-export'], :flag, 'Export current data instead of importing'
41
    option ['--csv-file'], 'FILE_NAME', 'CSV file (default to /dev/stdout with --csv-export, otherwise required)'
42
    option ['--prefix'], 'PREFIX', 'Prefix for all name columns'
43
    option ['--server'], 'SERVER', 'Server URL'
44
    option ['-u', '--username'], 'USERNAME', 'Username to access server'
45
    option ['-p', '--password'], 'PASSWORD', 'Password to access server'
46

    
47
    def execute
48
      if !option_csv_file
49
        if option_csv_export?
50
          option_csv_file = '/dev/stdout'
51
        else
52
          option_csv_file = '/dev/stdin'
53
        end
54
      end
55

    
56
      @init_options = {
57
        :base_url => option_server   || HammerCLI::Settings.get(:host),
58
        :username => option_username || HammerCLI::Settings.get(:username),
59
        :password => option_password || HammerCLI::Settings.get(:password)
60
      }
61

    
62
      @k_system_api ||= KatelloApi::Resources::System.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
63
      @k_systemgroup_api ||= KatelloApi::Resources::SystemGroup.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
64
      @k_environment_api ||= KatelloApi::Resources::Environment.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
65
      @k_contentview_api ||= KatelloApi::Resources::ContentView.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
66
      @k_provider_api ||= KatelloApi::Resources::Provider.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
67
      @k_product_api ||= KatelloApi::Resources::Product.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
68
      @k_repository_api ||= KatelloApi::Resources::Repository.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
69
      @k_contentviewdefinition_api ||= KatelloApi::Resources::ContentViewDefinition.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
70
      @k_subscription_api ||= KatelloApi::Resources::Subscription.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
71
      @k_organization_api ||= KatelloApi::Resources::Organization.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
72
      @k_activationkey_api ||= KatelloApi::Resources::ActivationKey.new(@init_options.merge({:base_url => "#{@init_options[:base_url]}"}))
73

    
74
      @f_architecture_api ||= ForemanApi::Resources::Architecture.new(@init_options)
75
      @f_domain_api ||= ForemanApi::Resources::Domain.new(@init_options)
76
      @f_environment_api ||= ForemanApi::Resources::Environment.new(@init_options)
77
      @f_filter_api ||= ForemanApi::Resources::Filter.new(@init_options)
78
      @f_host_api ||= ForemanApi::Resources::Host.new(@init_options)
79
      @f_location_api ||= ForemanApi::Resources::Location.new(@init_options)
80
      @f_operatingsystem_api ||= ForemanApi::Resources::OperatingSystem.new(@init_options)
81
      @f_organization_api ||= ForemanApi::Resources::Organization.new(@init_options)
82
      @f_permission_api ||= ForemanApi::Resources::Permission.new(@init_options)
83
      @f_partitiontable_api ||= ForemanApi::Resources::Ptable.new(@init_options)
84
      @f_puppetfacts_api ||= ForemanApi::Resources::FactValue.new(@init_options)
85
      @f_report_api ||= ForemanApi::Resources::Report.new(@init_options)
86
      @f_role_api ||= ForemanApi::Resources::Role.new(@init_options)
87
      @f_user_api ||= ForemanApi::Resources::User.new(@init_options)
88

    
89
      option_csv_export? ? export : import
90
      HammerCLI::EX_OK
91
    end
92

    
93
    def namify(name_format, number=0)
94
      if name_format.index('%')
95
        name = name_format % number
96
      else
97
        name = name_format
98
      end
99
      name = "#{option_prefix}#{name}" if option_prefix
100
      name
101
    end
102

    
103
    def labelize(name)
104
      name.gsub(/[^a-z0-9\-_]/i, "_")
105
    end
106

    
107
    def thread_import(return_headers=false)
108
      csv = []
109
      CSV.foreach(option_csv_file || '/dev/stdin', {:skip_blanks => true, :headers => :first_row, 
110
                    :return_headers => return_headers}) do |line|
111
        csv << line
112
      end
113
      lines_per_thread = csv.length/option_threads.to_i + 1
114
      splits = []
115

    
116
      option_threads.to_i.times do |current_thread|
117
        start_index = ((current_thread) * lines_per_thread).to_i
118
        finish_index = ((current_thread + 1) * lines_per_thread).to_i
119
        lines = csv[start_index...finish_index].clone
120
        splits << Thread.new do
121
          lines.each do |line|
122
            if line[NAME][0] != '#'
123
              yield line
124
            end
125
          end
126
        end
127
      end
128

    
129
      splits.each do |thread|
130
        thread.join
131
      end
132
    end
133

    
134
    def foreman_organization(options={})
135
      @organizations ||= {}
136

    
137
      if options[:name]
138
        return nil if options[:name].nil? || options[:name].empty?
139
        options[:id] = @organizations[options[:name]]
140
        if !options[:id]
141
          organization = @f_organization_api.index({'search' => "title=\"#{options[:name]}\""})[0]['results']
142
          raise RuntimeError, "Organization '#{options[:name]}' not found" if !organization || organization.empty?
143
          options[:id] = organization[0]['id']
144
          @organizations[options[:name]] = options[:id]
145
        end
146
        result = options[:id]
147
      else
148
        return nil if options[:id].nil?
149
        options[:name] = @organizations.key(options[:id])
150
        if !options[:name]
151
          organization = @f_organization_api.show({'id' => options[:id]})[0]
152
          raise "Organization 'id=#{options[:id]}' not found" if !organization || organization.empty?
153
          options[:name] = organization['name']
154
          @organizations[options[:name]] = options[:id]
155
        end
156
        result = options[:name]
157
      end
158

    
159
      result
160
    end
161

    
162
    def katello_organization(options={})
163
      @organizations ||= {}
164

    
165
      if options[:name]
166
        return nil if options[:name].nil? || options[:name].empty?
167
        options[:id] = @organizations[options[:name]]
168
        if !options[:id]
169
          organization = @k_organization_api.index({'search' => "title=\"#{options[:name]}\""})[0]['results']
170
          raise RuntimeError, "Organization '#{options[:name]}' not found" if !organization || organization.empty?
171
          options[:id] = organization[0]['label']
172
          @organizations[options[:name]] = options[:id]
173
        end
174
        result = options[:id]
175
      else
176
        return nil if options[:id].nil?
177
        options[:name] = @organizations.key(options[:id])
178
        if !options[:name]
179
          organization = @k_organization_api.show({'id' => options[:id]})[0]
180
          raise "Organization 'id=#{options[:id]}' not found" if !organization || organization.empty?
181
          options[:name] = organization['name']
182
          @organizations[options[:name]] = options[:id]
183
        end
184
        result = options[:name]
185
      end
186

    
187
      result
188
    end
189

    
190
    def foreman_location(options={})
191
      @locations ||= {}
192

    
193
      if options[:name]
194
        return nil if options[:name].nil? || options[:name].empty?
195
        options[:id] = @locations[options[:name]]
196
        if !options[:id]
197
          location = @f_location_api.index({'search' => "name=\"#{options[:name]}\""})[0]['results']
198
          raise RuntimeError, "Location '#{options[:name]}' not found" if !location || location.empty?
199
          options[:id] = location[0]['id']
200
          @locations[options[:name]] = options[:id]
201
        end
202
        result = options[:id]
203
      else
204
        return nil if options[:id].nil?
205
        options[:name] = @locations.key(options[:id])
206
        if !options[:name]
207
          location = @f_location_api.show({'id' => options[:id]})[0]
208
          raise "Location 'id=#{options[:id]}' not found" if !location || location.empty?
209
          options[:name] = location['name']
210
          @locations[options[:name]] = options[:id]
211
        end
212
        result = options[:name]
213
      end
214

    
215
      result
216
    end
217

    
218
    def foreman_role(options={})
219
      @roles ||= {}
220

    
221
      if options[:name]
222
        return nil if options[:name].nil? || options[:name].empty?
223
        options[:id] = @roles[options[:name]]
224
        if !options[:id]
225
          role = @f_role_api.index({'search' => "name=\"#{options[:name]}\""})[0]['results']
226
          raise RuntimeError, "Role '#{options[:name]}' not found" if !role || role.empty?
227
          options[:id] = role[0]['id']
228
          @roles[options[:name]] = options[:id]
229
        end
230
        result = options[:id]
231
      else
232
        return nil if options[:id].nil?
233
        options[:name] = @roles.key(options[:id])
234
        if !options[:name]
235
          role = @f_role_api.show({'id' => options[:id]})[0]
236
          raise "Role 'id=#{options[:id]}' not found" if !role || role.empty?
237
          options[:name] = role['name']
238
          @roles[options[:name]] = options[:id]
239
        end
240
        result = options[:name]
241
      end
242

    
243
      result
244
    end
245

    
246
    def foreman_permission(options={})
247
      @permissions ||= {}
248

    
249
      if options[:name]
250
        return nil if options[:name].nil? || options[:name].empty?
251
        options[:id] = @permissions[options[:name]]
252
        if !options[:id]
253
          permission = @f_permission_api.index({'name' => options[:name]})[0]['results']
254
          raise RuntimeError, "Permission '#{options[:name]}' not found" if !permission || permission.empty?
255
          options[:id] = permission[0]['id']
256
          @permissions[options[:name]] = options[:id]
257
        end
258
        result = options[:id]
259
      else
260
        return nil if options[:id].nil?
261
        options[:name] = @permissions.key(options[:id])
262
        if !options[:name]
263
          permission = @f_permission_api.show({'id' => options[:id]})[0]
264
          raise "Permission 'id=#{options[:id]}' not found" if !permission || permission.empty?
265
          options[:name] = permission['name']
266
          @permissions[options[:name]] = options[:id]
267
        end
268
        result = options[:name]
269
      end
270

    
271
      result
272
    end
273

    
274
    def foreman_filter(role, options={})
275
      @filters ||= {}
276

    
277
      if options[:name]
278
        return nil if options[:name].nil? || options[:name].empty?
279
        options[:id] = @filters[options[:name]]
280
        if !options[:id]
281
          filter = @f_filter_api.index({'search' => "role=\"#{role}\" and search=\"#{options[:name]}\""})[0]['results']
282
          if !filter || filter.empty?
283
            options[:id] = nil
284
          else
285
            options[:id] = filter[0]['id']
286
            @filters[options[:name]] = options[:id]
287
          end
288
        end
289
        result = options[:id]
290
      else
291
        return nil if options[:id].nil?
292
        options[:name] = @filters.key(options[:id])
293
        if !options[:name]
294
          filter = @f_filter_api.show({'id' => options[:id]})[0]
295
          raise "Filter 'id=#{options[:id]}' not found" if !filter || filter.empty?
296
          options[:name] = filter['name']
297
          @filters[options[:name]] = options[:id]
298
        end
299
        result = options[:name]
300
      end
301

    
302
      result
303
    end
304

    
305
    def foreman_environment(options={})
306
      @environments ||= {}
307

    
308
      if options[:name]
309
        return nil if options[:name].nil? || options[:name].empty?
310
        options[:id] = @environments[options[:name]]
311
        if !options[:id]
312
          environment = @f_environment_api.index({'search' => "name=\"#{options[:name]}\""})[0]['results']
313
          raise "Puppet environment '#{options[:name]}' not found" if !environment || environment.empty?
314
          options[:id] = environment[0]['id']
315
          @environments[options[:name]] = options[:id]
316
        end
317
        result = options[:id]
318
      else
319
        return nil if options[:id].nil?
320
        options[:name] = @environments.key(options[:id])
321
        if !options[:name]
322
          environment = @f_environment_api.show({'id' => options[:id]})[0]
323
          raise "Puppet environment '#{options[:name]}' not found" if !environment || environment.empty?
324
          options[:name] = environment['name']
325
          @environments[options[:name]] = options[:id]
326
        end
327
        result = options[:name]
328
      end
329

    
330
      result
331
    end
332

    
333
    def foreman_operatingsystem(options={})
334
      @operatingsystems ||= {}
335

    
336
      if options[:name]
337
        return nil if options[:name].nil? || options[:name].empty?
338
        options[:id] = @operatingsystems[options[:name]]
339
        if !options[:id]
340
          (osname, major, minor) = split_os_name(options[:name])
341
          search = "name=\"#{osname}\" and major=\"#{major}\" and minor=\"#{minor}\""
342
          operatingsystems = @f_operatingsystem_api.index({'search' => search})[0]['results']
343
          operatingsystem = operatingsystems[0]
344
          raise "Operating system '#{options[:name]}' not found" if !operatingsystem || operatingsystem.empty?
345
          options[:id] = operatingsystem['id']
346
          @operatingsystems[options[:name]] = options[:id]
347
        end
348
        result = options[:id]
349
      else
350
        return nil if options[:id].nil?
351
        options[:name] = @operatingsystems.key(options[:id])
352
        if !options[:name]
353
          operatingsystem = @f_operatingsystem_api.show({'id' => options[:id]})[0]
354
          raise "Operating system 'id=#{options[:id]}' not found" if !operatingsystem || operatingsystem.empty?
355
          options[:name] = build_os_name(operatingsystem['name'],
356
                                         operatingsystem['major'],
357
                                         operatingsystem['minor'])
358
          @operatingsystems[options[:name]] = options[:id]
359
        end
360
        result = options[:name]
361
      end
362

    
363
      result
364
    end
365

    
366
    def foreman_architecture(options={})
367
      @architectures ||= {}
368

    
369
      if options[:name]
370
        return nil if options[:name].nil? || options[:name].empty?
371
        options[:id] = @architectures[options[:name]]
372
        if !options[:id]
373
          architecture = @f_architecture_api.index({'search' => "name=\"#{options[:name]}\""})[0]['results']
374
          raise "Architecture '#{options[:name]}' not found" if !architecture || architecture.empty?
375
          options[:id] = architecture[0]['id']
376
          @architectures[options[:name]] = options[:id]
377
        end
378
        result = options[:id]
379
      else
380
        return nil if options[:id].nil?
381
        options[:name] = @architectures.key(options[:id])
382
        if !options[:name]
383
          architecture = @f_architecture_api.show({'id' => options[:id]})[0]
384
          raise "Architecture 'id=#{options[:id]}' not found" if !architecture || architecture.empty?
385
          options[:name] = architecture['name']
386
          @architectures[options[:name]] = options[:id]
387
        end
388
        result = options[:name]
389
      end
390

    
391
      result
392
    end
393

    
394
    def foreman_domain(options={})
395
      @domains ||= {}
396

    
397
      if options[:name]
398
        return nil if options[:name].nil? || options[:name].empty?
399
        options[:id] = @domains[options[:name]]
400
        if !options[:id]
401
          domain = @f_domain_api.index({'search' => "name=\"#{options[:name]}\""})[0]['results']
402
          raise "Domain '#{options[:name]}' not found" if !domain || domain.empty?
403
          options[:id] = domain[0]['id']
404
          @domains[options[:name]] = options[:id]
405
        end
406
        result = options[:id]
407
      else
408
        return nil if options[:id].nil?
409
        options[:name] = @domains.key(options[:id])
410
        if !options[:name]
411
          domain = @f_domain_api.show({'id' => options[:id]})[0]
412
          raise "Domain 'id=#{options[:id]}' not found" if !domain || domain.empty?
413
          options[:name] = domain['name']
414
          @domains[options[:name]] = options[:id]
415
        end
416
        result = options[:name]
417
      end
418

    
419
      result
420
    end
421

    
422
    def foreman_partitiontable(options={})
423
      @ptables ||= {}
424

    
425
      if options[:name]
426
        return nil if options[:name].nil? || options[:name].empty?
427
        options[:id] = @ptables[options[:name]]
428
        if !options[:id]
429
          ptable = @f_partitiontable_api.index({'search' => "name=\"#{options[:name]}\""})[0]['results']
430
          raise "Partition table '#{options[:name]}' not found" if !ptable || ptable.empty?
431
          options[:id] = ptable[0]['id']
432
          @ptables[options[:name]] = options[:id]
433
        end
434
        result = options[:id]
435
      elsif options[:id]
436
        return nil if options[:id].nil?
437
        options[:name] = @ptables.key(options[:id])
438
        if !options[:name]
439
          ptable = @f_partitiontable_api.show({'id' => options[:id]})[0]
440
          options[:name] = ptable['name']
441
          @ptables[options[:name]] = options[:id]
442
        end
443
        result = options[:name]
444
      elsif !options[:name] && !options[:id]
445
        result = ''
446
      end
447

    
448
      result
449
    end
450

    
451
    def katello_environment(organization, options={})
452
      @environments ||= {}
453
      @environments[organization] ||= {}
454

    
455
      if options[:name]
456
        return nil if options[:name].nil? || options[:name].empty?
457
        options[:id] = @environments[organization][options[:name]]
458
        if !options[:id]
459
          @k_environment_api.index({'organization_id' => katello_organization(:name => organization)})[0]['results'].each do |environment|
460
            @environments[organization][environment['name']] = environment['id']
461
          end
462
          options[:id] = @environments[organization][options[:name]]
463
          raise "Lifecycle environment '#{options[:name]}' not found" if !options[:id]
464
        end
465
        result = options[:id]
466
      else
467
        return nil if options[:id].nil?
468
        options[:name] = @environments.key(options[:id])
469
        if !options[:name]
470
          environment = @k_environment_api.show({'id' => options[:id]})[0]
471
          raise "Lifecycle environment '#{options[:name]}' not found" if !environment || environment.empty?
472
          options[:name] = environment['name']
473
          @environments[options[:name]] = options[:id]
474
        end
475
        result = options[:name]
476
      end
477

    
478
      result
479
    end
480

    
481
    def katello_contentview(organization, options={})
482
      @contentviews ||= {}
483
      @contentviews[organization] ||= {}
484

    
485
      if options[:name]
486
        return nil if options[:name].nil? || options[:name].empty?
487
        options[:id] = @contentviews[organization][options[:name]]
488
        if !options[:id]
489
          @k_contentview_api.index({'organization_id' => katello_organization(:name => organization)})[0]['results'].each do |contentview|
490
            @contentviews[organization][contentview['name']] = contentview['id']
491
          end
492
          options[:id] = @contentviews[organization][options[:name]]
493
          raise "Content view '#{options[:name]}' not found" if !options[:id]
494
        end
495
        result = options[:id]
496
      else
497
        return nil if options[:id].nil?
498
        options[:name] = @contentviews.key(options[:id])
499
        if !options[:name]
500
          contentview = @k_contentview_api.show({'id' => options[:id]})[0]
501
          raise "Puppet contentview '#{options[:name]}' not found" if !contentview || contentview.empty?
502
          options[:name] = contentview['name']
503
          @contentviews[options[:name]] = options[:id]
504
        end
505
        result = options[:name]
506
      end
507

    
508
      result
509
    end
510

    
511
    def katello_subscription(organization, options={})
512
      @subscriptions ||= {}
513
      @subscriptions[organization] ||= {}
514

    
515
      if options[:name]
516
        return nil if options[:name].nil? || options[:name].empty?
517
        options[:id] = @subscriptions[organization][options[:name]]
518
        if !options[:id]
519
          results = @k_subscription_api.index({
520
                                                'organization_id' => katello_organization(:name => organization),
521
                                                'search' => "name:\"#{options[:name]}\""
522
                                              })[0]
523
          raise "No subscriptions match '#{options[:name]}'" if results['subtotal'] == 0
524
          raise "Too many subscriptions match '#{options[:name]}'" if results['subtotal'] > 1
525
          subscription = results['results'][0]
526
          @subscriptions[organization][options[:name]] = subscription['id']
527
          options[:id] = @subscriptions[organization][options[:name]]
528
          raise "Subscription '#{options[:name]}' not found" if !options[:id]
529
        end
530
        result = options[:id]
531
      else
532
        return nil if options[:id].nil?
533
        options[:name] = @subscriptions.key(options[:id])
534
        if !options[:name]
535
          subscription = @k_subscription_api.show({'id' => options[:id]})[0]
536
          raise "Subscription '#{options[:name]}' not found" if !subscription || subscription.empty?
537
          options[:name] = subscription['name']
538
          @subscriptions[options[:name]] = options[:id]
539
        end
540
        result = options[:name]
541
      end
542

    
543
      result
544
    end
545

    
546
    def katello_systemgroup(organization, options={})
547
      @systemgroups ||= {}
548
      @systemgroups[organization] ||= {}
549

    
550
      if options[:name]
551
        return nil if options[:name].nil? || options[:name].empty?
552
        options[:id] = @systemgroups[organization][options[:name]]
553
        if !options[:id]
554
          @k_systemgroup_api.index({
555
                                     'organization_id' => katello_organization(:name => organization),
556
                                     'search' => "name:\"#{options[:name]}\""
557
                                   })[0]['results'].each do |systemgroup|
558
            @systemgroups[organization][systemgroup['name']] = systemgroup['id'] if systemgroup
559
          end
560
          options[:id] = @systemgroups[organization][options[:name]]
561
          raise "System group '#{options[:name]}' not found" if !options[:id]
562
        end
563
        result = options[:id]
564
      else
565
        return nil if options[:id].nil?
566
        options[:name] = @systemgroups.key(options[:id])
567
        if !options[:name]
568
          systemgroup = @k_systemgroup_api.show({'id' => options[:id]})[0]
569
          raise "System group '#{options[:name]}' not found" if !systemgroup || systemgroup.empty?
570
          options[:name] = systemgroup['name']
571
          @systemgroups[options[:name]] = options[:id]
572
        end
573
        result = options[:name]
574
      end
575

    
576
      result
577
    end
578

    
579
    def build_os_name(name, major, minor)
580
      name += " #{major}" if major && major != ""
581
      name += ".#{minor}" if minor && minor != ""
582
      name
583
    end
584

    
585
    # "Red Hat 6.4" => "Red Hat", "6", "4"
586
    # "Red Hat 6"   => "Red Hat", "6", ""
587
    def split_os_name(name)
588
      tokens = name.split(' ')
589
      is_number = Float(tokens[-1]) rescue false
590
      if is_number
591
        (major, minor) = tokens[-1].split('.').flatten
592
        name = tokens[0...-1].join(' ')
593
      else
594
        name = tokens.join(' ')
595
      end
596
      [name, major || "", minor || ""]
597
    end
598
  end
599
end