lib/rmt/scc.rb

Summary

Maintainability
A
35 mins
Test Coverage
require 'rmt/config'
require 'rmt/logger'
require 'suse/connect/api'

class RMT::SCC
  class CredentialsError < RuntimeError; end
  class DataFilesError < RuntimeError; end

  def initialize(options = {})
    @logger = RMT::Logger.new(STDOUT)
    debug = options[:debug] || Settings&.log_level&.cli == 'debug'
    @logger.level = debug ? Logger::DEBUG : Logger::INFO
  end

  def sync
    credentials_set? || (raise CredentialsError, _('SCC credentials not set.'))

    @logger.info(_('Downloading data from SCC'))
    scc_api_client = SUSE::Connect::Api.new(Settings.scc.username, Settings.scc.password)

    data = scc_api_client.list_products
    @logger.info(_('Updating products'))
    data.each { |item| create_product(item) }
    data.each { |item| migration_paths(item) }

    update_repositories(scc_api_client.list_repositories)

    Repository.remove_suse_repos_without_tokens!

    update_subscriptions(scc_api_client.list_subscriptions)
  end

  def export(path)
    credentials_set? || (raise CredentialsError, 'SCC credentials not set.')

    @logger.info _('Exporting data from SCC to %{path}') % { path: path }

    scc_api_client = SUSE::Connect::Api.new(Settings.scc.username, Settings.scc.password)

    @logger.info(_('Exporting products'))
    File.write(File.join(path, 'organizations_products.json'), scc_api_client.list_products.to_json)
    # For SUMA, we also export the unscoped products with the filename it expects.
    File.write(File.join(path, 'organizations_products_unscoped.json'), scc_api_client.list_products_unscoped.to_json)

    @logger.info(_('Exporting repositories'))
    File.write(File.join(path, 'organizations_repositories.json'), scc_api_client.list_repositories.to_json)

    @logger.info(_('Exporting subscriptions'))
    File.write(File.join(path, 'organizations_subscriptions.json'), scc_api_client.list_subscriptions.to_json)

    @logger.info(_('Exporting orders'))
    File.write(File.join(path, 'organizations_orders.json'), scc_api_client.list_orders.to_json)
  end

  def import(path)
    missing_files = %w[products repositories subscriptions]
      .map { |data| "organizations_#{data}.json" }
      .reject { |filename| File.exist?(File.join(path, filename)) }
    raise DataFilesError, _('Missing data files: %{files}') % { files: missing_files.join(', ') } if missing_files.any?

    @logger.info _('Importing SCC data from %{path}') % { path: path }

    @logger.info _('Updating products')
    data = JSON.parse(File.read(File.join(path, 'organizations_products.json')), symbolize_names: true)
    data.each do |item|
      create_product(item)
    end
    data.each { |item| migration_paths(item) }

    update_repositories(JSON.parse(File.read(File.join(path, 'organizations_repositories.json')), symbolize_names: true))

    Repository.remove_suse_repos_without_tokens!

    update_subscriptions(JSON.parse(File.read(File.join(path, 'organizations_subscriptions.json')), symbolize_names: true))
  end

  def sync_systems
    if Settings.scc.sync_systems == false
      @logger.warn _('Syncing systems to SCC is disabled by the configuration file, exiting.')
      return
    end

    credentials_set? || (raise CredentialsError, _('SCC credentials not set.'))
    scc_api_client = SUSE::Connect::Api.new(Settings.scc.username, Settings.scc.password)

    # do not sync BYOS proxy systems to SCC
    systems = System.where('scc_registered_at IS NULL OR last_seen_at > scc_registered_at').where(proxy_byos: false)
    @logger.info(_('Syncing %{count} updated system(s) to SCC') % { count: systems.size })

    begin
      updated_systems = scc_api_client.send_bulk_system_update(systems)
    rescue StandardError => e
      @logger.error(_('Failed to sync systems: %{error}') % { error: e.to_s })
    else
      failed_scc_synced_systems = systems.pluck(:login).excluding(updated_systems[:systems].pluck(:login))
      if failed_scc_synced_systems.present?
        # The response from SCC will be 201 even if some single systems failed to save.
        @logger.info(_("Couldn't sync %{count} systems.") % { count: failed_scc_synced_systems.count })
      end

      updated_systems[:systems].each do |system_hash|
        # In RMT - SCC communication, RMT's system id is used as token, see also lib/suse/connect/api.rb:108
        system = if system_hash[:system_token]
                   System.find_by(id: system_hash[:system_token])
                 else
                   System.find_by(login: system_hash[:login])
                 end
        system.update_columns(
          scc_system_id: system_hash[:id],
          scc_synced_at: Time.current
        )
      end
    end
    DeregisteredSystem.find_in_batches(batch_size: 20) do |batch|
      batch.each do |deregistered_system|
        @logger.info(
          _('Syncing de-registered system %{scc_system_id} to SCC') % {
            scc_system_id: deregistered_system.scc_system_id
          }
        )
        scc_api_client.forward_system_deregistration(deregistered_system.scc_system_id)
        deregistered_system.destroy!
      end
    end
  end

  protected

  def credentials_set?
    Settings.try(:scc).try(:username) && Settings.try(:scc).try(:password)
  end

  def update_repositories(repos)
    @logger.info _('Updating repositories')
    repos.each do |repo|
      repository_service.update_repository!(repo)
    end
  end

  def update_subscriptions(subscriptions)
    @logger.info _('Updating subscriptions')
    subscriptions.each do |item|
      subscription = Subscription.find_or_create_by(id: item[:id])
      subscription.attributes = item.select { |k, _| subscription.attributes.keys.member?(k.to_s) }
      subscription.kind = item[:type]
      subscription.save!

      item[:product_classes].each do |item_class|
        SubscriptionProductClass.find_or_create_by(subscription_id: subscription.id, product_class: item_class)
      end
    end
  end

  def get_product(id)
    Product.find_or_create_by(id: id)
  end

  def create_product(item, root_product_id = nil, base_product = nil, recommended = false, migration_extra = false)
    ActiveRecord::Base.transaction do
      @logger.debug _('Adding/Updating product %{product}') % { product: "#{item[:identifier]}/#{item[:version]}#{(item[:arch]) ? '/' + item[:arch] : ''}" }

      product = get_product(item[:id])
      product.attributes = item.select { |k, _| product.attributes.keys.member?(k.to_s) }
      product.save!

      create_service(item, product)

      if root_product_id
        ProductsExtensionsAssociation.create(
          product_id: base_product,
          extension_id: product.id,
          root_product_id: root_product_id,
          recommended: recommended,
          migration_extra: migration_extra
        )
      else
        root_product_id = product.id
        ProductsExtensionsAssociation.where(root_product_id: root_product_id).destroy_all
      end

      item[:extensions].each do |ext_item|
        create_product(ext_item, root_product_id, product.id, ext_item[:recommended], ext_item[:migration_extra])
      end
    end
  end

  def create_service(item, product)
    product.create_service!
    item[:repositories].each do |repo_item|
      repository_service.create_repository!(product, repo_item[:url], repo_item)
    end
  end

  def migration_paths(item)
    product = get_product(item[:id])
    ProductPredecessorAssociation.where(product_id: product.id).destroy_all
    create_migration_path(product, item[:online_predecessor_ids], :online) unless item[:online_predecessor_ids].empty?
    create_migration_path(product, item[:offline_predecessor_ids], :offline) unless item[:offline_predecessor_ids].empty?
    item[:extensions].each do |ext_item|
      migration_paths(ext_item)
    end
  end

  def create_migration_path(product, predecessors, kind)
    predecessors.each do |predecessor_id|
      ProductPredecessorAssociation.create(product_id: product.id, predecessor_id: predecessor_id, kind: kind) unless Product.find_by(id: predecessor_id).nil?
    end
  end

  private

  def repository_service
    @repository_service ||= RepositoryService.new
  end

end