Project

General

Profile

Revision c06f1783

Added by Thomas McKay over 6 years ago

fixes #15743 - import and export of subscriptions one-per-line

View differences:

lib/hammer_cli_csv/content_hosts.rb
6 6
      command_name 'content-hosts'
7 7
      desc         'import or export content hosts'
8 8

  
9
      def self.supported?
10
        true
11
      end
12

  
13
      option %w(--subscriptions-only), :flag, 'Export only detailed subscription information'
14

  
9 15
      ORGANIZATION = 'Organization'
10 16
      ENVIRONMENT = 'Environment'
11 17
      CONTENTVIEW = 'Content View'
......
20 26
      SLA = 'SLA'
21 27
      PRODUCTS = 'Products'
22 28
      SUBSCRIPTIONS = 'Subscriptions'
29
      SUBS_NAME = 'Subscription Name'
30
      SUBS_TYPE = 'Subscription Type'
31
      SUBS_QUANTITY = 'Subscription Quantity'
32
      SUBS_SKU = 'Subscription SKU'
33
      SUBS_CONTRACT = 'Subscription Contract'
34
      SUBS_ACCOUNT = 'Subscription Account'
35
      SUBS_START = 'Subscription Start'
36
      SUBS_END = 'Subscription End'
23 37

  
24
      def export
25
        CSV.open(option_file || '/dev/stdout', 'wb', {:force_quotes => false}) do |csv|
26
          csv << [NAME, ORGANIZATION, ENVIRONMENT, CONTENTVIEW, HOSTCOLLECTIONS, VIRTUAL, HOST,
27
                  OPERATINGSYSTEM, ARCHITECTURE, SOCKETS, RAM, CORES, SLA, PRODUCTS, SUBSCRIPTIONS]
28
          export_katello csv
38
      def export(csv)
39
        if option_subscriptions_only?
40
          export_subscriptions csv
41
        else
42
          export_all csv
29 43
        end
30 44
      end
31 45

  
32
      def export_katello(csv)
33
        @api.resource(:organizations).call(:index, {:per_page => 999999})['results'].each do |organization|
34
          next if option_organization && organization['name'] != option_organization
35

  
36
          @api.resource(:hosts).call(:index, {
37
              'per_page' => 999999,
38
              'organization_id' => foreman_organization(:name => organization['name'])
39
          })['results'].each do |host|
40
            host = @api.resource(:hosts).call(:show, {
41
                'id' => host['id']
42
            })
43
            host['facts'] ||= {}
44

  
45
            name = host['name']
46
            organization_name = organization['name']
47
            if host['content_facet_attributes']
48
              environment = host['content_facet_attributes']['lifecycle_environment']['name']
49
              contentview = host['content_facet_attributes']['content_view']['name']
50
              hostcollections = export_column(host['content_facet_attributes'], 'host_collections', 'name')
46
      def export_subscriptions(csv)
47
        csv << shared_headers + [SUBS_NAME, SUBS_TYPE, SUBS_QUANTITY, SUBS_SKU,
48
                                 SUBS_CONTRACT, SUBS_ACCOUNT, SUBS_START, SUBS_END]
49
        iterate_hosts(csv) do |host|
50
          export_line = shared_columns(host)
51
          if host['subscription_facet_attributes']
52
            subscriptions = @api.resource(:host_subscriptions).call(:index, {
53
                'organization_id' => host['organization_id'],
54
                'host_id' => host['id']
55
            })['results']
56
            if subscriptions.empty?
57
              csv << export_line + [nil, nil, nil, nil, nil, nil]
51 58
            else
52
              environment = nil
53
              contentview = nil
54
              hostcollections = nil
59
              subscriptions.each do |subscription|
60
                subscription_type = subscription['product_id'].to_i == 0 ? 'Red Hat' : 'Custom'
61
                subscription_type += ' Guest' if subscription['type'] == 'STACK_DERIVED'
62
                subscription_type += ' Temporary' if subscription['type'] == 'UNMAPPED_GUEST'
63
                csv << export_line + [subscription['product_name'], subscription_type,
64
                                      subscription['quantity_consumed'], subscription['product_id'],
65
                                      subscription['contract_number'], subscription['account_number'],
66
                                      DateTime.parse(subscription['start_date']).strftime('%m/%d/%Y'),
67
                                      DateTime.parse(subscription['end_date']).strftime('%m/%d/%Y')]
68
              end
55 69
            end
56
            virtual = host['facts']['virt::is_guest'] == 'true' ? 'Yes' : 'No'
57
            hypervisor_host = host['subscription_facet_attributes']['virtual_host'].nil? ? nil : host['subscription_facet_attributes']['virtual_host']['name']
58
            operatingsystem = host['facts']['distribution::name'] if host['facts']['distribution::name']
59
            operatingsystem += " #{host['facts']['distribution::version']}" if host['facts']['distribution::version']
60
            architecture = host['facts']['uname::machine']
61
            sockets = host['facts']['cpu::cpu_socket(s)']
62
            ram = host['facts']['memory::memtotal']
63
            cores = host['facts']['cpu::core(s)_per_socket'] || 1
64
            sla = ''
65
            products = export_column(host['subscription_facet_attributes'], 'installed_products', 'productName')
70
          else
71
            csv << export_line + [nil, nil, nil, nil, nil, nil]
72
          end
73
        end
74
      end
75

  
76
      def export_all(csv)
77
        csv << shared_headers + [SUBSCRIPTIONS]
78
        iterate_hosts(csv) do |host|
79
          if host['subscription_facet_attributes']
66 80
            subscriptions = CSV.generate do |column|
67 81
              column << @api.resource(:host_subscriptions).call(:index, {
68
                  'organization_id' => organization['id'],
82
                  'organization_id' => host['organization_id'],
69 83
                  'host_id' => host['id']
70 84
              })['results'].collect do |subscription|
71 85
                "#{subscription['quantity_consumed']}"\
......
75 89
              end
76 90
            end
77 91
            subscriptions.delete!("\n")
78
            csv << [name, organization_name, environment, contentview, hostcollections, virtual, hypervisor_host,
79
                    operatingsystem, architecture, sockets, ram, cores, sla, products, subscriptions]
92
          else
93
            subscriptions = nil
80 94
          end
95

  
96
          csv << shared_columns(host) + [subscriptions]
81 97
        end
82 98
      end
83 99

  
......
99 115
      def import_locally
100 116
        @existing = {}
101 117
        @hypervisor_guests = {}
118
        @all_subscriptions = {}
102 119

  
103 120
        thread_import do |line|
104 121
          create_from_csv(line)
......
111 128
              'id' => host_id,
112 129
              'host' => {
113 130
                'subscription_facet_attributes' => {
131
                  'autoheal' => false,
114 132
                  'hypervisor_guest_uuids' => guest_ids
115 133
                }
116 134
              }
......
128 146
        count(line[COUNT]).times do |number|
129 147
          name = namify(line[NAME], number)
130 148

  
131
          if !@existing.include? name
132
            print(_("Creating content host '%{name}'...") % {:name => name}) if option_verbose?
133
            params = {
134
              'name' => name,
135
              'facts' => facts(name, line),
136
              'lifecycle_environment_id' => lifecycle_environment(line[ORGANIZATION], :name => line[ENVIRONMENT]),
137
              'content_view_id' => katello_contentview(line[ORGANIZATION], :name => line[CONTENTVIEW]),
138
              'installed_products' => products(line),
139
              'service_level' => line[SLA]
140
            }
141
            host = @api.resource(:host_subscriptions).call(:create, params)
142
            @existing[name] = host['id']
149
          if option_subscriptions_only?
150
            update_subscriptions_only(name, line)
143 151
          else
144
            print(_("Updating content host '%{name}'...") % {:name => name}) if option_verbose?
145
            params = {
146
              'id' => @existing[name],
147
              'host' => {
148
                'content_facet_attributes' => {
149
                  'lifecycle_environment_id' => lifecycle_environment(line[ORGANIZATION], :name => line[ENVIRONMENT]),
150
                  'content_view_id' => katello_contentview(line[ORGANIZATION], :name => line[CONTENTVIEW])
151
                },
152
                'subscription_facet_attributes' => {
153
                  'facts' => facts(name, line),
154
                  'installed_products' => products(line),
155
                  'service_level' => line[SLA]
156
                }
157
              }
158
            }
159
            host = @api.resource(:hosts).call(:update, params)
152
            update_or_create(name, line)
160 153
          end
154
        end
155
      end
161 156

  
162
          if line[VIRTUAL] == 'Yes' && line[HOST]
163
            raise "Content host '#{line[HOST]}' not found" if !@existing[line[HOST]]
164
            @hypervisor_guests[@existing[line[HOST]]] ||= []
165
            @hypervisor_guests[@existing[line[HOST]]] << "#{line[ORGANIZATION]}/#{name}"
166
          end
157
      private
167 158

  
168
          update_host_collections(host, line)
169
          update_subscriptions(host, line)
159
      def update_subscriptions_only(name, line)
160
        raise _("Content host '%{name}' must already exist with --subscriptions-only") % {:name => name} unless @existing.include? name
170 161

  
171
          puts _('done') if option_verbose?
162
        print(_("Updating subscriptions for content host '%{name}'...") % {:name => name}) if option_verbose?
163
        host = @api.resource(:hosts).call(:show, {:id => @existing[name]})
164
        update_subscriptions(host, line, false)
165
        puts _('done') if option_verbose?
166
      end
167

  
168
      def update_or_create(name, line)
169
        if !@existing.include? name
170
          print(_("Creating content host '%{name}'...") % {:name => name}) if option_verbose?
171
          params = {
172
            'name' => name,
173
            'facts' => facts(name, line),
174
            'lifecycle_environment_id' => lifecycle_environment(line[ORGANIZATION], :name => line[ENVIRONMENT]),
175
            'content_view_id' => katello_contentview(line[ORGANIZATION], :name => line[CONTENTVIEW])
176
          }
177
          params['installed_products'] = products(line) if line[PRODUCTS]
178
          params['service_level'] = line[SLA] if line[SLA]
179
          host = @api.resource(:host_subscriptions).call(:create, params)
180
          @existing[name] = host['id']
181
        else
182
          print(_("Updating content host '%{name}'...") % {:name => name}) if option_verbose?
183
          params = {
184
            'id' => @existing[name],
185
            'host' => {
186
              'content_facet_attributes' => {
187
                'lifecycle_environment_id' => lifecycle_environment(line[ORGANIZATION], :name => line[ENVIRONMENT]),
188
                'content_view_id' => katello_contentview(line[ORGANIZATION], :name => line[CONTENTVIEW])
189
              },
190
              'subscription_facet_attributes' => {
191
                'facts' => facts(name, line),
192
                'installed_products' => products(line),
193
                'service_level' => line[SLA]
194
              }
195
            }
196
          }
197
          host = @api.resource(:hosts).call(:update, params)
198
        end
199

  
200
        if line[VIRTUAL] == 'Yes' && line[HOST]
201
          raise "Content host '#{line[HOST]}' not found" if !@existing[line[HOST]]
202
          @hypervisor_guests[@existing[line[HOST]]] ||= []
203
          @hypervisor_guests[@existing[line[HOST]]] << "#{line[ORGANIZATION]}/#{name}"
172 204
        end
205

  
206
        update_host_collections(host, line)
207
        update_subscriptions(host, line, true)
208

  
209
        puts _('done') if option_verbose?
173 210
      rescue RuntimeError => e
174 211
        raise "#{e}\n       #{line}"
175 212
      end
176 213

  
177
      private
178

  
179 214
      def facts(name, line)
180 215
        facts = {}
181 216
        facts['system.certificate_version'] = '3.2'  # Required for auto-attach to work
......
192 227
      end
193 228

  
194 229
      def update_host_collections(host, line)
195
        return nil if !line[HOSTCOLLECTIONS]
196
        CSV.parse_line(line[HOSTCOLLECTIONS]).each do |hostcollection_name|
197
          @api.resource(:host_collections).call(:add_hosts, {
198
              'id' => katello_hostcollection(line[ORGANIZATION], :name => hostcollection_name),
199
              'host_ids' => [host['id']]
200
          })
201
        end
230
        # TODO: http://projects.theforeman.org/issues/16234
231
        # return nil if line[HOSTCOLLECTIONS].nil? || line[HOSTCOLLECTIONS].empty?
232
        # CSV.parse_line(line[HOSTCOLLECTIONS]).each do |hostcollection_name|
233
        #   @api.resource(:host_collections).call(:add_hosts, {
234
        #       'id' => katello_hostcollection(line[ORGANIZATION], :name => hostcollection_name),
235
        #       'host_ids' => [host['id']]
236
        #   })
237
        # end
202 238
      end
203 239

  
204 240
      def os_name_version(operatingsystem)
......
224 260
        end
225 261
      end
226 262

  
227
      def update_subscriptions(host, line)
263
      def update_subscriptions(host, line, remove_existing)
228 264
        existing_subscriptions = @api.resource(:host_subscriptions).call(:index, {
229 265
            'host_id' => host['id']
230 266
        })['results']
231
        if existing_subscriptions.length != 0
267
        if remove_existing && existing_subscriptions.length != 0
268
          existing_subscriptions.map! do |existing_subscription|
269
            {:id => existing_subscription['id'], :quantity => existing_subscription['quantity_consumed']}
270
          end
232 271
          @api.resource(:host_subscriptions).call(:remove_subscriptions, {
233 272
            'host_id' => host['id'],
234 273
            'subscriptions' => existing_subscriptions
235 274
          })
275
          existing_subscriptions = []
276
        end
277

  
278
        if line[SUBS_NAME].nil? && line[SUBS_SKU].nil?
279
          all_in_one_subscription(host, existing_subscriptions, line)
280
        else
281
          single_subscription(host, existing_subscriptions, line)
236 282
        end
283
      end
284

  
285
      def single_subscription(host, existing_subscriptions, line)
286
        already_attached = false
287
        if line[SUBS_SKU]
288
          already_attached = existing_subscriptions.detect do |subscription|
289
            line[SUBS_SKU] == subscription['product_id']
290
          end
291
        elsif line[SUBS_NAME]
292
          already_attached = existing_subscriptions.detect do |subscription|
293
            line[SUBS_NAME] == subscription['name']
294
          end
295
        end
296
        if already_attached
297
          print _(" '%{name}' already attached...") % {:name => already_attached['name']}
298
          return
299
        end
300

  
301
        available_subscriptions = @api.resource(:subscriptions).call(:index, {
302
          'organization_id' => host['organization_id'],
303
          'host_id' => host['id'],
304
          'available_for' => 'host',
305
          'match_host' => true
306
        })['results']
307

  
308
        matches = matches_by_sku_and_name([], line, available_subscriptions)
309
        matches = matches_by_type(matches, line)
310
        matches = matches_by_account(matches, line)
311
        matches = matches_by_contract(matches, line)
312
        matches = matches_by_quantity(matches, line)
237 313

  
314
        raise _("No matching subscriptions") if matches.empty?
315

  
316
        match = matches[0]
317
        print _(" attaching '%{name}'...") % {:name => match['name']} if option_verbose?
318

  
319
        @api.resource(:host_subscriptions).call(:add_subscriptions, {
320
            'host_id' => host['id'],
321
            'subscriptions' => existing_subscriptions + [match]
322
        })
323
      end
324

  
325
      def all_in_one_subscription(host, existing_subscriptions, line)
238 326
        return if line[SUBSCRIPTIONS].nil? || line[SUBSCRIPTIONS].empty?
239 327

  
240 328
        subscriptions = CSV.parse_line(line[SUBSCRIPTIONS], {:skip_blanks => true}).collect do |details|
......
274 362
          end
275 363
        end
276 364
      end
365

  
366
      def get_all_subscriptions(organization)
367
        @api.resource(:subscriptions).call(:index, {
368
            :per_page => 999999,
369
            'organization_id' => foreman_organization(:name => organization)
370
        })['results']
371
      end
372

  
373
      def iterate_hosts(csv)
374
        hypervisors = []
375
        hosts = []
376
        @api.resource(:organizations).call(:index, {:per_page => 999999})['results'].each do |organization|
377
          next if option_organization && organization['name'] != option_organization
378

  
379
          @api.resource(:hosts).call(:index, {
380
              'per_page' => 999999,
381
              'organization_id' => foreman_organization(:name => organization['name'])
382
          })['results'].each do |host|
383
            host = @api.resource(:hosts).call(:show, {
384
                'id' => host['id']
385
            })
386
            host['facts'] ||= {}
387
            if host['subscription_facet_attributes']['virtual_guests'].empty?
388
              hosts.push(host)
389
            else
390
              hypervisors.push(host)
391
            end
392
          end
393
        end
394
        hypervisors.each do |host|
395
          yield host
396
        end
397
        hosts.each do |host|
398
          yield host
399
        end
400
      end
401

  
402
      def shared_headers
403
        [NAME, ORGANIZATION, ENVIRONMENT, CONTENTVIEW, HOSTCOLLECTIONS, VIRTUAL, HOST,
404
         OPERATINGSYSTEM, ARCHITECTURE, SOCKETS, RAM, CORES, SLA, PRODUCTS]
405
      end
406

  
407
      def shared_columns(host)
408
        name = host['name']
409
        organization_name = host['organization_name']
410
        if host['content_facet_attributes']
411
          environment = host['content_facet_attributes']['lifecycle_environment']['name']
412
          contentview = host['content_facet_attributes']['content_view']['name']
413
          hostcollections = export_column(host['content_facet_attributes'], 'host_collections', 'name')
414
        else
415
          environment = nil
416
          contentview = nil
417
          hostcollections = nil
418
        end
419
        if host['subscription_facet_attributes']
420
          hypervisor_host = host['subscription_facet_attributes']['virtual_host'].nil? ? nil : host['subscription_facet_attributes']['virtual_host']['name']
421
          products = export_column(host['subscription_facet_attributes'], 'installed_products') do |product|
422
            "#{product['productId']}|#{product['productName']}"
423
          end
424
        else
425
          hypervisor_host = nil
426
          products = nil
427
        end
428
        virtual = host['facts']['virt::is_guest'] == 'true' ? 'Yes' : 'No'
429
        operatingsystem = host['facts']['distribution::name'] if host['facts']['distribution::name']
430
        operatingsystem += " #{host['facts']['distribution::version']}" if host['facts']['distribution::version']
431
        architecture = host['facts']['uname::machine']
432
        sockets = host['facts']['cpu::cpu_socket(s)']
433
        ram = host['facts']['memory::memtotal']
434
        cores = host['facts']['cpu::core(s)_per_socket'] || 1
435
        sla = ''
436

  
437
        [name, organization_name, environment, contentview, hostcollections, virtual, hypervisor_host,
438
         operatingsystem, architecture, sockets, ram, cores, sla, products]
439
      end
440

  
441
      def matches_by_sku_and_name(matches, line, subscriptions)
442
        if line[SUBS_SKU]
443
          matches = subscriptions.select do |subscription|
444
            line[SUBS_SKU] == subscription['product_id']
445
          end
446
          raise _("No subscriptions match SKU '%{sku}'") % {:sku => line[SUBS_SKU]} if matches.empty?
447
        elsif line[SUBS_NAME]
448
          matches = subscriptions.select do |subscription|
449
            line[SUBS_NAME] == subscription['name']
450
          end
451
          raise _("No subscriptions match name '%{name}'") % {:name => line[SUBS_NAME]} if matches.empty?
452
        end
453
        matches
454
      end
455

  
456
      def matches_by_type(matches, line)
457
        if line[SUBS_TYPE] == 'Red Hat' || line[SUBS_TYPE] == 'Custom'
458
          matches = matches.select do |subscription|
459
            subscription['type'] == 'NORMAL'
460
          end
461
        elsif line[SUBS_TYPE] == 'Red Hat Guest'
462
          matches = matches.select do |subscription|
463
            subscription['type'] == 'STACK_DERIVED'
464
          end
465
        elsif line[SUBS_TYPE] == 'Red Hat Temporary'
466
          matches = matches.select do |subscription|
467
            subscription['type'] == 'UNMAPPED_GUEST'
468
          end
469
        end
470
        raise _("No subscriptions match type '%{type}'") % {:type => line[SUBS_TYPE]} if matches.empty?
471
        matches
472
      end
473

  
474
      def matches_by_account(matches, line)
475
        if matches.length > 1 && line[SUBS_ACCOUNT]
476
          refined = matches.select do |subscription|
477
            line[SUBS_ACCOUNT] == subscription['account_number']
478
          end
479
          matches = refined unless refined.empty?
480
        end
481
        matches
482
      end
483

  
484
      def matches_by_contract(matches, line)
485
        if matches.length > 1 && line[SUBS_CONTRACT]
486
          refined = matches.select do |subscription|
487
            line[SUBS_CONTRACT] == subscription['contract_number']
488
          end
489
          matches = refined unless refined.empty?
490
        end
491
        matches
492
      end
493

  
494
      def matches_by_quantity(matches, line)
495
        if line[SUBS_QUANTITY] && line[SUBS_QUANTITY] != 'Automatic'
496
          refined = matches.select do |subscription|
497
            subscription['available'] == -1 || line[SUBS_QUANTITY].to_i <= subscription['available']
498
          end
499
          raise _("No '%{name}' subscription with quantity %{quantity} or more available") %
500
                    {:name => matches[0]['name'], :quantity => line[SUBS_QUANTITY]} if refined.empty?
501
          matches = refined
502
        end
503
        matches
504
      end
277 505
    end
278 506
  end
279 507
end

Also available in: Unified diff