diff --git a/.env.example b/.env.example
index 2229a9035..68abdc7b0 100644
--- a/.env.example
+++ b/.env.example
@@ -2,3 +2,5 @@ SECRET_KEY_BASE=8ea53ad3bc6c03923e376c8bdd85059c1885524947a7efe53d5e9c9d4e398611
EXTERNAL_PORT=8080
SCC_USERNAME=
SCC_PASSWORD=
+RMT_METRICS_ENABLED=
+PROMETHEUS_JOB_NAME=
diff --git a/.github/workflows/lint-unit.yml b/.github/workflows/lint-unit.yml
index fc5842d18..e97009ec4 100644
--- a/.github/workflows/lint-unit.yml
+++ b/.github/workflows/lint-unit.yml
@@ -81,8 +81,13 @@ jobs:
- name: Run core tests
run: |
- bundle exec rspec --format documentation
+ bundle exec rake test:core
+ - name: Run core tests with sqlite
+ run: |
+ sed -i 's/adapter: mysql2/adapter: sqlite3/' config/rmt.yml
+ bundle exec rake test:core
+
- name: Run PubCloud engines tests
run: |
bundle exec rake test:engines
@@ -91,4 +96,4 @@ jobs:
run: |
echo "::group::Version verification checks"
ruby ci/check-version-matches.rb
- echo "::endgroup::"
+ echo "::endgroup::"
\ No newline at end of file
diff --git a/.simplecov b/.simplecov
index e08fb8ee6..052bcb2d8 100644
--- a/.simplecov
+++ b/.simplecov
@@ -10,6 +10,10 @@ unless ENV['NO_COVERAGE']
# omit registration sharing (removing systems using rmt-cli)
add_filter('engines/registration_sharing/lib/registration_sharing.rb')
+ add_filter('lib/rmt/db.rb')
+ add_filter('lib/rmt.rb')
+ add_filter('config')
+
track_files('app/**/*.rb')
track_files('lib/**/*.rb')
end
diff --git a/Gemfile b/Gemfile
index 552fb8205..4f97e5013 100644
--- a/Gemfile
+++ b/Gemfile
@@ -32,6 +32,12 @@ gem 'repomd_parser', '~> 1.1.0'
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'
+# Prometheus Exporter:
+gem 'yabeda'
+gem 'yabeda-rails'
+gem 'yabeda-puma-plugin'
+gem 'yabeda-prometheus'
+
gem 'strong_migrations'
group :development, :test do
@@ -43,7 +49,7 @@ group :development, :test do
gem 'gettext', require: false # needed for gettext_i18n_rails tasks
gem 'ruby_parser', '< 3.20', require: false # needed for gettext_i18n_rails tasks, Locked because of Ruby >= 2.6 dependency
gem 'gettext_test_log'
- gem 'memory_profiler'
+ gem 'memory_profiler', '~> 1.0.2' # locked because 1.1.0 requires ruby version >= 3.1.0
gem 'awesome_print'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 9ccead6eb..93d77c0ed 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -30,8 +30,10 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
- addressable (2.8.6)
- public_suffix (>= 2.0.2, < 6.0)
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ anyway_config (2.6.4)
+ ruby-next-core (~> 1.0)
ast (2.4.2)
awesome_print (1.9.2)
base32 (0.3.4)
@@ -136,13 +138,13 @@ GEM
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
- hashdiff (1.1.0)
+ hashdiff (1.1.1)
hpricot (0.8.6)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
json (2.3.1)
jsonapi-renderer (0.2.2)
- jwt (2.8.2)
+ jwt (2.9.3)
base64
listen (3.6.0)
rb-fsevent (~> 0.10, >= 0.10.3)
@@ -160,7 +162,7 @@ GEM
mustache (1.1.1)
mysql2 (0.5.6)
nenv (0.3.0)
- nio4r (2.7.0)
+ nio4r (2.7.3)
nokogiri (1.12.5)
mini_portile2 (~> 2.6.1)
racc (~> 1.4)
@@ -173,11 +175,13 @@ GEM
prime (0.1.2)
forwardable
singleton
+ prometheus-client (4.2.3)
+ base64
pry (0.14.0)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (4.0.7)
- puma (5.6.8)
+ puma (5.6.9)
nio4r (~> 2.0)
racc (1.8.0)
rack (2.2.9)
@@ -209,8 +213,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
- rexml (3.3.6)
- strscan
+ rexml (3.3.9)
ronn (0.7.3)
hpricot (>= 0.8.2)
mustache (>= 0.7.0)
@@ -267,6 +270,7 @@ GEM
rubocop (~> 1.19)
rubocop-thread_safety (0.4.4)
rubocop (>= 0.53.0)
+ ruby-next-core (1.0.3)
ruby-progressbar (1.11.0)
ruby-xz (1.0.3)
ruby_parser (3.19.2)
@@ -297,7 +301,6 @@ GEM
sqlite3 (1.4.4)
strong_migrations (0.7.9)
activerecord (>= 5)
- strscan (3.1.0)
sync (0.5.0)
term-ansicolor (1.7.1)
tins (~> 1.0)
@@ -318,10 +321,27 @@ GEM
activesupport (>= 3)
railties (>= 3)
yard (~> 0.9.20)
- webmock (3.23.1)
+ webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
+ yabeda (0.13.1)
+ anyway_config (>= 1.0, < 3)
+ concurrent-ruby
+ dry-initializer
+ yabeda-prometheus (0.9.1)
+ prometheus-client (>= 3.0, < 5.0)
+ rack
+ yabeda (~> 0.10)
+ yabeda-puma-plugin (0.7.1)
+ json
+ puma
+ yabeda (~> 0.5)
+ yabeda-rails (0.9.0)
+ activesupport
+ anyway_config (>= 1.3, < 3)
+ railties
+ yabeda (~> 0.8)
yard (0.9.35)
zeitwerk (2.6.15)
zstd-ruby (1.5.6.1)
@@ -352,7 +372,7 @@ DEPENDENCIES
guard-rspec
jwt (~> 2.1)
listen (>= 3.0.5, <= 3.6.0)
- memory_profiler
+ memory_profiler (~> 1.0.2)
minitest (~> 5.15.0)
mysql2 (~> 0.5.3)
nokogiri (< 1.13)
@@ -384,6 +404,10 @@ DEPENDENCIES
vcr (~> 6.0)
versionist
webmock
+ yabeda
+ yabeda-prometheus
+ yabeda-puma-plugin
+ yabeda-rails
BUNDLED WITH
1.17.3
diff --git a/PACKAGE.md b/PACKAGE.md
index c58ab2695..557f750d0 100644
--- a/PACKAGE.md
+++ b/PACKAGE.md
@@ -21,7 +21,7 @@ Note: Look below for direction on publishing to registry.
```
* Alternatively, if an OBS working copy is already checked out, update the working copy by running `osc up`
2. Run `make dist` in your RMT working directory to build a tarball.
-3. Copy the files from the `package/obs` directory to the OBS working directory.
+3. Copy the files from the `package/obs` directory to the OBS working directory `systemsmanagement:SCC:RMT/rmt-server`.
4. Examine the changes by running `osc status` and `osc diff`.
5. Stage the changes by running `osc addremove`.
6. Build the package with osc:
@@ -109,4 +109,4 @@ RMT helm chart is defined [here](https://github.com/SUSE/helm-charts.git) and pu
Edit `rmt-helm/Chart.yaml` to update the chart version (`version`) and rmt-version (`appVersion`). The `BuildTag` version needs to be updated. Look at this example [pull-request](https://github.com/SUSE/helm-charts/pull/5) bumping the version.
-Please reach out to the BCI team if you have build related questions.
+Reach out to the BCI team (Dirk Mueller or `#proj-bci` slack channel) to trigger the release of the helm-chart.
diff --git a/app/controllers/api/connect/base_controller.rb b/app/controllers/api/connect/base_controller.rb
index 1fbeaff07..84de46574 100644
--- a/app/controllers/api/connect/base_controller.rb
+++ b/app/controllers/api/connect/base_controller.rb
@@ -44,4 +44,14 @@ def authenticate_with_token
end
end
+ def system_token_header
+ headers[SYSTEM_TOKEN_HEADER] = @system.system_token
+ end
+
+ def refresh_system_token
+ if system_tokens_enabled?
+ @system.update(system_token: SecureRandom.uuid)
+ system_token_header
+ end
+ end
end
diff --git a/app/controllers/api/connect/v3/systems/products_controller.rb b/app/controllers/api/connect/v3/systems/products_controller.rb
index 2545a9e92..648443ecd 100644
--- a/app/controllers/api/connect/v3/systems/products_controller.rb
+++ b/app/controllers/api/connect/v3/systems/products_controller.rb
@@ -5,6 +5,7 @@ class Api::Connect::V3::Systems::ProductsController < Api::Connect::BaseControll
before_action :check_product_service_and_repositories, only: %i[show activate]
before_action :load_subscription, only: %i[activate upgrade]
before_action :check_base_product_dependencies, only: %i[activate upgrade show]
+ after_action :refresh_system_token, only: %i[activate upgrade], if: -> { request.headers.key?(SYSTEM_TOKEN_HEADER) }
def activate
create_product_activation
@@ -12,7 +13,13 @@ def activate
end
def show
- if @system.products.include? @product
+ if @product.identifier.casecmp?('sles')
+ # if system has SLE Micro
+ # it should access to SLES products
+ sle_micro = @system.products.any? { |p| p.identifier.downcase.include?('sle-micro') }
+ sle_micro_same_arch = @system.products.pluck(:arch).include?(@product.arch) if sle_micro
+ end
+ if @system.products.include?(@product) || sle_micro_same_arch
respond_with(
@product,
serializer: ::V3::ProductSerializer,
diff --git a/app/controllers/api/connect/v3/systems/systems_controller.rb b/app/controllers/api/connect/v3/systems/systems_controller.rb
index b7622e252..d9a17154d 100644
--- a/app/controllers/api/connect/v3/systems/systems_controller.rb
+++ b/app/controllers/api/connect/v3/systems/systems_controller.rb
@@ -1,6 +1,7 @@
class Api::Connect::V3::Systems::SystemsController < Api::Connect::BaseController
before_action :authenticate_system
+ after_action :refresh_system_token, only: [:update], if: -> { request.headers.key?(SYSTEM_TOKEN_HEADER) }
def update
if params[:online_at].present?
@@ -21,8 +22,8 @@ def update
# Since the payload is handled by rails all values are converted to string
# e.g. cpus: 16 becomes cpus: "16". We save this as string for now and expect
- # SCC to handle the convertation correctly
- @system.system_information = hwinfo_params[:hwinfo].to_json
+ # SCC to handle the conversion correctly
+ @system.system_information = @system.system_information_hash.update(hwinfo_params[:hwinfo]).to_json
if @system.save
logger.info(N_("Updated system information for host '%s'") % @system.hostname)
diff --git a/app/controllers/api/connect/v4/systems/products_controller.rb b/app/controllers/api/connect/v4/systems/products_controller.rb
index 7237e5a80..d4c87adfc 100644
--- a/app/controllers/api/connect/v4/systems/products_controller.rb
+++ b/app/controllers/api/connect/v4/systems/products_controller.rb
@@ -1,5 +1,6 @@
class Api::Connect::V4::Systems::ProductsController < Api::Connect::V3::Systems::ProductsController
+ after_action :refresh_system_token, only: %i[activate upgrade synchronize destroy], if: -> { request.headers.key?(SYSTEM_TOKEN_HEADER) }
def destroy
if @product.base?
raise ActionController::TranslatedError.new(N_('The product "%s" is a base product and cannot be deactivated'), @product.name)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 12eaadef0..63458615a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -18,12 +18,13 @@ def authenticate_system(skip_on_duplicated: false)
return true if skip_on_duplicated && @systems.size > 1
@system = find_system_by_token_header(@systems)
+ update_user_agent
# If SYSTEM_TOKEN_HEADER is present, RMT assumes the client uses a SUSEConnect version
- # that supports this feature. In this case, refresh the token and include it in the response.
+ # that supports this feature.
if system_tokens_enabled? && request.headers.key?(SYSTEM_TOKEN_HEADER)
- @system.update(last_seen_at: Time.zone.now, system_token: SecureRandom.uuid)
- headers[SYSTEM_TOKEN_HEADER] = @system.system_token
+ @system.update(last_seen_at: Time.zone.now)
+ system_token_header
# only update last_seen_at each 3 minutes,
# so that a system that calls SCC every second doesn't write + lock the database row
elsif !@system.last_seen_at || @system.last_seen_at < 3.minutes.ago
@@ -42,6 +43,15 @@ def authenticate_system(skip_on_duplicated: false)
private
+ def zypper_request?
+ user_agent = request.headers['HTTP_USER_AGENT']
+ user_agent&.downcase&.starts_with?('zypp')
+ end
+
+ def update_user_agent
+ @system.set_system_information('user_agent', request.headers['HTTP_USER_AGENT']) unless zypper_request?
+ end
+
# Token mechanism to detect duplicated systems.
# 1: system doesn't send a token header (old SUSEConnect version)
# 2: system sends a token, and it matches an existing system with that token
diff --git a/app/controllers/services_controller.rb b/app/controllers/services_controller.rb
index e28d94cbe..74f6d2f65 100644
--- a/app/controllers/services_controller.rb
+++ b/app/controllers/services_controller.rb
@@ -14,12 +14,10 @@ class ServicesController < ApplicationController
# authenticate requests on this method for Zypper so we have a better picture
# which systems are still being active (even if not using SUSEConnect).
before_action only: %w[show] do
- ua = request.headers['HTTP_USER_AGENT']
-
# Zypper will never provide the `system_token` credentials for the system.
# Hence, if there are duplicates, we will not be able to deterministically
# tell which system is to be updated. Just skip it altogether on this case.
- authenticate_system(skip_on_duplicated: true) if ua && ua.downcase.starts_with?('zypp')
+ authenticate_system(skip_on_duplicated: true) if zypper_request?
end
ZYPPER_SERVICE_TTL = 86400
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6a7719aa2..c65c602cd 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -23,14 +23,15 @@ class Repository < ApplicationRecord
class << self
def remove_suse_repos_without_tokens!
- where(auth_token: nil).where('external_url LIKE ?', 'https://updates.suse.com%').delete_all
+ where(auth_token: nil).where("external_url LIKE '%.suse.com%'").where(installer_updates: 0).where.not(scc_id: nil).delete_all
end
# Mangles remote repo URL to make a nicer local path, see specs for examples
def make_local_path(url)
uri = URI(url)
path = uri.path.to_s
- path.gsub!(%r{^/repo}, '') if (uri.hostname == 'updates.suse.com')
+ # drop '/repo' from SLE11 paths, to avoid double /repo/repo in local storage path.
+ path.gsub!(%r{^/repo/\$RCE/}, '/$RCE/')
(path == '') ? '/' : path
end
diff --git a/app/models/system.rb b/app/models/system.rb
index be98e602b..2d9f97808 100644
--- a/app/models/system.rb
+++ b/app/models/system.rb
@@ -37,10 +37,17 @@ def self.generate_secure_login
end
def cloud_provider
+ system_information_hash.fetch(:cloud_provider, nil)
+ end
+
+ def system_information_hash
# system_information is checked for valid JSON on save. It is safe
# to assume the structure is valid.
- info = JSON.parse(system_information).symbolize_keys
- info.fetch(:cloud_provider, nil)
+ JSON.parse(system_information || '{}').symbolize_keys
+ end
+
+ def set_system_information(key, value)
+ update(system_information: system_information_hash.update(key => value).to_json)
end
# Generate secure token for System password
diff --git a/app/serializers/v3/product_serializer.rb b/app/serializers/v3/product_serializer.rb
index d0ccea8b4..90747f838 100644
--- a/app/serializers/v3/product_serializer.rb
+++ b/app/serializers/v3/product_serializer.rb
@@ -33,8 +33,11 @@ def eula_url
end
def free
- # Everything is free on RMT :-)
+ # Everything is free on RMT :-) outside of the Public Cloud (i.e. LTSS)
# Otherwise Yast and SUSEConnect will request a regcode when activating an extension
+ # FIXME
+ return object.free if defined?(SccProxy::Engine) && object.extension?
+
true
end
diff --git a/config/initializers/yabeda.rb b/config/initializers/yabeda.rb
new file mode 100644
index 000000000..0396c27e8
--- /dev/null
+++ b/config/initializers/yabeda.rb
@@ -0,0 +1,38 @@
+# :nocov:
+# frozen_string_literal: true
+
+return unless Settings.dig(:scc, :metrics, :enabled)
+
+# Configure prometheus client
+Prometheus::Client.config.data_store = Prometheus::Client::DataStores::DirectFileStore.new(
+ dir: './tmp/prometheus/'
+)
+
+# Configure yabeda
+Yabeda.configure do
+ assign_labels = lambda {
+ default_tag :environment, Rails.env
+ default_tag :application, 'rmt'
+ }
+
+ group :rails, &assign_labels
+
+ group :rails do
+ counter :started_requests_total,
+ comment: 'A counter of the total number of HTTP requests rails has started to process.',
+ tags: %i[controller action format method]
+ end
+end
+
+# Instrument the request from the start
+ActiveSupport::Notifications.subscribe 'start_processing.action_controller' do |*args|
+ # Match the same event as Yabeda
+ event = Yabeda::Rails::Event.new(*args)
+
+ Yabeda.rails.started_requests_total.tap do |metric|
+ labels = event.labels.slice(*metric.tags)
+
+ metric.increment(labels, by: 1)
+ end
+end
+# :nocov:
diff --git a/config/puma.rb b/config/puma.rb
index c1fec1414..39ace4d2b 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -1,4 +1,5 @@
require 'rmt/config'
+require_relative 'puma/prometheus'
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
@@ -36,3 +37,7 @@
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
+
+if Settings.dig(:scc, :metrics, :enabled)
+ configure_prometheus!(self)
+end
diff --git a/config/puma/prometheus.rb b/config/puma/prometheus.rb
new file mode 100644
index 000000000..df5dcd9b9
--- /dev/null
+++ b/config/puma/prometheus.rb
@@ -0,0 +1,7 @@
+def configure_prometheus!(puma)
+ ENV['STARTED_FROM_PUMA'] = '1'
+
+ puma.activate_control_app
+ puma.plugin :yabeda
+ puma.plugin :yabeda_prometheus
+end
diff --git a/config/rmt.yml b/config/rmt.yml
index 3b1dabb2f..6a2df5b5c 100644
--- a/config/rmt.yml
+++ b/config/rmt.yml
@@ -20,10 +20,13 @@ database_test:
database: rmt_test
scc:
- host: https://scc.suse.com/connect
+ host: <%= ENV.fetch('SCC_HOST'){ 'https://scc.suse.com/connect' } %>
username: <%= ENV['SCC_USERNAME'] %>
password: <%= ENV['SCC_PASSWORD'] %>
sync_systems: true
+ metrics:
+ enabled: <%= ENV.fetch('RMT_METRICS_ENABLED') { false } %>
+ job_name: <%= ENV.fetch('PROMETHEUS_JOB_NAME') { 'rmt-webserver' } %>
mirroring:
mirror_src: false
diff --git a/config/routes.rb b/config/routes.rb
index 9cea050f2..7c43d4b87 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -69,6 +69,12 @@
get '/v2/_catalog', to: 'registry/registry#catalog'
end
+ # For bin/rails s users: expose the metrics endpoint via rails instead of puma
+ if Rails.env.development? && ENV['STARTED_FROM_PUMA'].blank?
+ Rails.logger.info('Mounting Yabeda in Rails.routes')
+ mount Yabeda::Prometheus::Exporter, at: '/metrics'
+ end
+
if defined?(SccSumaApi::Engine)
mount SccSumaApi::Engine, at: '/api/scc'
diff --git a/db/migrate/20240821114908_change_local_path_type.rb b/db/migrate/20240821114908_change_local_path_type.rb
index 83e0789e9..5c2f53cce 100644
--- a/db/migrate/20240821114908_change_local_path_type.rb
+++ b/db/migrate/20240821114908_change_local_path_type.rb
@@ -1,8 +1,8 @@
class ChangeLocalPathType < ActiveRecord::Migration[6.1]
def up
safety_assured do
- change_column :repositories, :local_path, :text
- change_column :downloaded_files, :local_path, :text
+ change_column :repositories, :local_path, :string, limit: 512
+ change_column :downloaded_files, :local_path, :string, limit: 512
end
end
diff --git a/db/schema.rb b/db/schema.rb
index bf4ce342e..1584f1814 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -34,7 +34,7 @@
create_table "downloaded_files", charset: "utf8", force: :cascade do |t|
t.string "checksum_type"
t.string "checksum"
- t.text "local_path"
+ t.string "local_path", limit: 512
t.bigint "file_size", unsigned: true
t.index ["checksum_type", "checksum"], name: "index_downloaded_files_on_checksum_type_and_checksum"
t.index ["local_path"], name: "index_downloaded_files_on_local_path", unique: true
@@ -104,7 +104,7 @@
t.string "auth_token"
t.boolean "installer_updates", default: false, null: false
t.boolean "mirroring_enabled", default: false, null: false
- t.text "local_path", null: false
+ t.string "local_path", limit: 512, null: false
t.datetime "last_mirrored_at"
t.string "friendly_id"
t.index ["external_url"], name: "index_repositories_on_external_url", unique: true
diff --git a/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb b/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb
index 9d00eee26..0217e54d7 100644
--- a/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb
+++ b/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb
@@ -5,7 +5,7 @@ def check
# belongs to a PAYG or BYOS instance
verification_provider = InstanceVerification.provider.new(
logger,
- nil,
+ request,
nil,
params[:metadata]
)
diff --git a/engines/instance_verification/lib/instance_verification/providers/example.rb b/engines/instance_verification/lib/instance_verification/providers/example.rb
index b773d7855..e4379c39b 100644
--- a/engines/instance_verification/lib/instance_verification/providers/example.rb
+++ b/engines/instance_verification/lib/instance_verification/providers/example.rb
@@ -27,10 +27,6 @@ def validate_instance_data(_instance_data)
end
def parse_instance_data
- if @instance_data.include? ''
- return { 'instance_data' => 'parsed_instance_data' }
- end
-
if @instance_data.include?('SUSE')
if @instance_data.include?('SAP')
return { 'billingProducts' => nil, 'marketplaceProductCodes' => ['6789_SUSE_SAP'] }
@@ -49,4 +45,14 @@ def payg_billing_code?(iid, identifier)
return true if (identifier.casecmp('sles').zero? && instance_billing_info[:billing_product] == SLES_PRODUCT_IDENTIFIER)
return true if (identifier.casecmp('sles_sap').zero? && SLES4SAP_PRODUCT_IDENTIFIER.include?(instance_billing_info[:marketplace_code]))
end
+
+ def instance_identifier
+ 'foo'
+ end
+
+ def allowed_extension?
+ # method to check if a product (extension) meets the criteria
+ # to be activated on SCC or not, i.e. LTSS in Azure Basic VM
+ true
+ end
end
diff --git a/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb b/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb
index 261775111..6b7afad35 100644
--- a/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb
+++ b/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb
@@ -38,7 +38,7 @@
it 'class instance verification provider' do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original
+ .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original.at_least(:once)
allow(File).to receive(:directory?)
allow(Dir).to receive(:mkdir)
allow(FileUtils).to receive(:touch)
@@ -71,10 +71,18 @@
end
context 'when verification provider returns false' do
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+
before do
- expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double)
- expect(plugin_double).to receive(:instance_valid?).and_return(false)
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 200,
+ body: { error: 'Unexpected instance verification error has occurred' }.to_json,
+ headers: {}
+ )
+ allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double)
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
+ allow(plugin_double).to receive(:instance_valid?).and_return(false)
post url, params: payload, headers: headers
end
@@ -86,9 +94,13 @@
context 'when verification provider raises an unhandled exception' do
before do
- expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double)
- expect(plugin_double).to receive(:instance_valid?).and_raise('Custom plugin error')
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 422,
+ body: { error: 'Unexpected instance verification error has occurred' }.to_json,
+ headers: {}
+ )
+
post url, params: payload, headers: headers
end
@@ -106,7 +118,7 @@
it 'class instance verification provider' do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original
+ .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original.at_least(:once)
allow(File).to receive(:directory?)
allow(Dir).to receive(:mkdir)
allow(FileUtils).to receive(:touch)
@@ -134,8 +146,9 @@
context 'when verification provider returns false' do
before do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double)
+ .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once)
expect(plugin_double).to receive(:instance_valid?).and_return(false)
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
post url, params: payload, headers: headers
end
@@ -148,8 +161,9 @@
context 'when verification provider raises an unhandled exception' do
before do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double)
+ .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once)
expect(plugin_double).to receive(:instance_valid?).and_raise('Custom plugin error')
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
post url, params: payload, headers: headers
end
@@ -164,9 +178,9 @@
before do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double)
+ .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once)
expect(plugin_double).to receive(:instance_valid?).and_raise(InstanceVerification::Exception, 'Custom plugin error')
-
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
post url, params: payload, headers: headers
end
@@ -220,9 +234,9 @@
end
before do
- allow(InstanceVerification::Providers::Example).to receive(:new)
- .with(nil, nil, nil, instance_data).and_return(plugin_double)
+ allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double)
allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' })
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
FactoryBot.create(:subscription, product_classes: product_classes)
stub_request(:post, scc_activate_url)
@@ -315,7 +329,7 @@
let(:scc_annouce_body) do
{
hostname: system.hostname,
- hwinfo: JSON.parse(system.system_information).merge({ instance_data: system.instance_data }),
+ hwinfo: JSON.parse(system.system_information),
byos_mode: 'hybrid',
login: system.login,
password: system.password
@@ -332,9 +346,10 @@
end
before do
- allow(InstanceVerification::Providers::Example).to receive(:new)
- .with(nil, nil, nil, instance_data).and_return(plugin_double)
+ allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double)
+ allow(plugin_double).to receive(:instance_identifier).and_return('foo')
allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' })
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
allow(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, product.id)
FactoryBot.create(:subscription, product_classes: product_classes)
@@ -373,8 +388,9 @@
before do
allow(InstanceVerification::Providers::Example).to receive(:new)
- .with(nil, nil, nil, instance_data).and_return(plugin_double)
+ .and_return(plugin_double)
allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' })
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
allow(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, product.id)
FactoryBot.create(:subscription, product_classes: product_classes)
@@ -390,7 +406,7 @@
.to_return(status: 201, body: scc_response_body, headers: {})
expect(InstanceVerification).not_to receive(:update_cache).with('127.0.0.1', system.login, product.id)
-
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
post url, params: payload_no_token, headers: headers
end
@@ -402,12 +418,16 @@
end
context 'when the system is hybrid' do
+ before do
+ allow_any_instance_of(InstanceVerification::Providers::Example).to receive(:allowed_extension?).and_return(true)
+ end
+
context "when system doesn't have hw_info" do
let(:system) { FactoryBot.create(:system, :hybrid) }
it 'class instance verification provider' do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original
+ .and_call_original.at_least(:once)
allow(File).to receive(:directory?)
allow(Dir).to receive(:mkdir)
allow(FileUtils).to receive(:touch)
@@ -435,7 +455,8 @@
context 'when verification provider returns false' do
before do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double)
+ .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once)
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
expect(plugin_double).to receive(:instance_valid?).and_return(false)
post url, params: payload, headers: headers
end
@@ -449,7 +470,8 @@
context 'when verification provider raises an unhandled exception' do
before do
expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double)
+ .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once)
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
expect(plugin_double).to receive(:instance_valid?).and_raise('Custom plugin error')
post url, params: payload, headers: headers
end
@@ -507,8 +529,9 @@
before do
allow(InstanceVerification::Providers::Example).to receive(:new)
- .with(nil, nil, nil, instance_data).and_return(plugin_double)
+ .and_return(plugin_double)
allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' })
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
FactoryBot.create(:subscription, product_classes: product_classes)
stub_request(:post, scc_activate_url)
@@ -540,6 +563,7 @@
context 'when no regcode is provided' do
it 'activates the product' do
data = JSON.parse(response.body)
+ expect(data['product']['free']).to eq(false)
expect(data['id']).to eq(product.id)
end
end
@@ -551,65 +575,176 @@
describe '#upgrade' do
subject { response }
- let(:system) { FactoryBot.create(:system) }
+ let(:instance_data) { 'dummy_instance_data' }
let(:request) { put url, headers: headers, params: payload }
- let!(:old_product) { FactoryBot.create(:product, :with_mirrored_repositories, :activated, system: system) }
- let(:payload) do
- {
- identifier: new_product.identifier,
- version: new_product.version,
- arch: new_product.arch
- }
- end
- before { request }
+ context 'when system is byos' do
+ let(:system) { FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data) }
+ let!(:old_product) { FactoryBot.create(:product, :with_mirrored_repositories, :activated, system: system) }
+ let(:payload) do
+ {
+ identifier: new_product.identifier,
+ version: new_product.version,
+ arch: new_product.arch
+ }
+ end
+ let(:scc_systems_products_url) { 'https://scc.suse.com/connect/systems/products' }
+ let(:scc_headers) do
+ {
+ 'Accept' => 'application/json,application/vnd.scc.suse.com.v4+json',
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
+ 'Authorization' => headers['HTTP_AUTHORIZATION'],
+ 'Content-Type' => 'application/json',
+ 'User-Agent' => 'Ruby'
+ }
+ end
+
+ context 'when SCC upgrade success' do
+ before do
+ # pp headers
+ stub_request(:put, scc_systems_products_url)
+ .with({ headers: scc_headers, body: payload.merge({ byos_mode: 'byos' }) })
+ .and_return(status: 201, body: '', headers: {})
+ request
+ end
+
+ context "when migration target base product doesn't have an activated successor/predecessor" do
+ let(:new_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+
+ it 'HTTP response code is 422' do
+ expect(response).to have_http_status(422)
+ end
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Migration target not allowed on this instance type')
+ end
+ end
+
+ context 'when migration target base product has the same identifier' do
+ let(:new_product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories, identifier: old_product.identifier,
+ version: '999', predecessors: [ old_product ]
+ )
+ end
- context "when migration target base product doesn't have an activated successor/predecessor" do
- let(:new_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+ it 'HTTP response code is 201' do
+ expect(response).to have_http_status(201)
+ end
- it 'HTTP response code is 422' do
- expect(response).to have_http_status(422)
+ it "doesn't render an error" do
+ data = JSON.parse(response.body)
+ expect(data).not_to have_key('error')
+ end
+ end
end
- it 'renders an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Migration target not allowed on this instance type')
+ context 'when SCC upgrade fails' do
+ before do
+ stub_request(:put, scc_systems_products_url)
+ .with({ headers: scc_headers, body: payload.merge({ byos_mode: 'byos' }) })
+ .and_return(
+ status: 401,
+ body: 'Migration target not allowed on this instance type',
+ headers: {}
+ )
+ request
+ end
+
+ context "when migration target base product doesn't have an activated successor/predecessor" do
+ let(:new_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+
+ it 'HTTP response code is 422' do
+ expect(response).to have_http_status(422)
+ end
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Migration target not allowed on this instance type')
+ end
+ end
+
+ context 'when migration target base product has the same identifier' do
+ let(:new_product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories, identifier: old_product.identifier,
+ version: '999', predecessors: [ old_product ]
+ )
+ end
+
+ it 'HTTP response code is 422' do
+ expect(response).to have_http_status(422)
+ end
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data).to have_key('error')
+ end
+ end
end
end
- context 'when migration target base product has a different identifier' do
- let(:new_product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories,
- identifier: old_product.identifier + '-foo', predecessors: [ old_product ]
- )
+ context 'when system is payg' do
+ let(:system) { FactoryBot.create(:system, :payg, :with_system_information, instance_data: instance_data) }
+ let!(:old_product) { FactoryBot.create(:product, :with_mirrored_repositories, :activated, system: system) }
+ let(:payload) do
+ {
+ identifier: new_product.identifier,
+ version: new_product.version,
+ arch: new_product.arch
+ }
end
- it 'HTTP response code is 422' do
- expect(response).to have_http_status(422)
- end
+ before { request }
- it 'renders an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Migration target not allowed on this instance type')
- end
- end
+ context "when migration target base product doesn't have an activated successor/predecessor" do
+ let(:new_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
- context 'when migration target base product has the same identifier' do
- let(:new_product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, identifier: old_product.identifier,
- version: '999', predecessors: [ old_product ]
- )
+ it 'HTTP response code is 422' do
+ expect(response).to have_http_status(422)
+ end
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Migration target not allowed on this instance type')
+ end
end
- it 'HTTP response code is 201' do
- expect(response).to have_http_status(201)
+ context 'when migration target base product has a different identifier' do
+ let(:new_product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories,
+ identifier: old_product.identifier + '-foo', predecessors: [ old_product ]
+ )
+ end
+
+ it 'HTTP response code is 422' do
+ expect(response).to have_http_status(422)
+ end
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Migration target not allowed on this instance type')
+ end
end
- it "doesn't render an error" do
- data = JSON.parse(response.body)
- expect(data).not_to have_key('error')
+ context 'when migration target base product has the same identifier' do
+ let(:new_product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories, identifier: old_product.identifier,
+ version: '999', predecessors: [ old_product ]
+ )
+ end
+
+ it 'HTTP response code is 201' do
+ expect(response).to have_http_status(201)
+ end
+
+ it "doesn't render an error" do
+ data = JSON.parse(response.body)
+ expect(data).not_to have_key('error')
+ end
end
end
end
diff --git a/engines/registry/app/models/access_scope.rb b/engines/registry/app/models/access_scope.rb
index fa565cdbb..b15c0b17b 100644
--- a/engines/registry/app/models/access_scope.rb
+++ b/engines/registry/app/models/access_scope.rb
@@ -87,15 +87,15 @@ def allowed_paths(system = nil)
allowed_product_classes = (active_product_classes & access_policies_yml.keys)
if system && system.hybrid?
# if the system is hybrid => check if the non free product subscription is still valid for accessing images
- allowed_non_free_product_classes = allowed_product_classes.map { |s| s unless Product.find_by(product_class: s).free? }
+ allowed_non_free_product_classes = allowed_product_classes.map { |s| s unless Product.find_by(product_class: s, product_type: 'extension').free? }.compact
unless allowed_non_free_product_classes.empty?
auth_header = {
- Authorization: ActionController::HttpAuthentication::Basic.encode_credentials(system.login, system.password)
+ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(system.login, system.password)
}
allowed_non_free_product_classes.each do |non_free_prod_class|
activation_state = SccProxy.scc_check_subscription_expiration(
- auth_header, system.login, system.system_token, Rails.logger, system.proxy_byos_mode, non_free_prod_class
- )
+ auth_header, system, non_free_prod_class
+ )
unless activation_state[:is_active]
Rails.logger.info(
"Access to #{non_free_prod_class} from system #{system.login} denied: #{activation_state[:message]}"
diff --git a/engines/registry/spec/app/models/access_scope_spec.rb b/engines/registry/spec/app/models/access_scope_spec.rb
index 0840efb53..0d3c84a22 100644
--- a/engines/registry/spec/app/models/access_scope_spec.rb
+++ b/engines/registry/spec/app/models/access_scope_spec.rb
@@ -194,7 +194,7 @@
system
end
let(:product1) do
- product = FactoryBot.create(:product, :with_mirrored_repositories)
+ product = FactoryBot.create(:product, :with_mirrored_repositories, :extension)
product.repositories.where(enabled: false).update(mirroring_enabled: false)
product.update(product_class: 'SLES15-SP4-LTSS-X86')
product
@@ -208,29 +208,22 @@
}
end
let(:header_expected) do
- { Authorization: ActionController::HttpAuthentication::Basic.encode_credentials(system.login, system.password) }
+ { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(system.login, system.password) }
end
before do
allow(SccProxy).to receive(:scc_check_subscription_expiration)
.with(
header_expected,
- system.login,
- system.system_token,
- Rails.logger,
- system.proxy_byos_mode,
+ system,
'SLES15-SP4-LTSS-X86'
).and_return(scc_response)
end
- # rubocop:disable RSpec/ExampleLength
it 'returns no actions allowed' do
expect(SccProxy).to receive(:scc_check_subscription_expiration).with(
header_expected,
- system.login,
- system.system_token,
- Rails.logger,
- system.proxy_byos_mode,
+ system,
'SLES15-SP4-LTSS-X86'
)
yaml_string = access_policy_content
@@ -250,7 +243,6 @@
}
)
end
- # rubocop:enable RSpec/ExampleLength
end
end
diff --git a/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb b/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
index a16896fbb..dd2cfcb73 100644
--- a/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
+++ b/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
@@ -3,7 +3,7 @@
include_context 'version header', 3
describe '#activations' do
- let(:system) { FactoryBot.create(:system, :with_activated_product) }
+ let(:system) { FactoryBot.create(:system, :payg, :with_activated_product) }
let(:headers) { auth_header.merge(version_header) }
context 'without valid repository cache' do
@@ -57,5 +57,68 @@
expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud})
end
end
+
+ context 'system is hybrid' do
+ let(:system) { FactoryBot.create(:system, :hybrid, :with_activated_product) }
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+ let(:cache_name) { "repo/cache/127.0.0.1-#{system.login}-#{system.products.first.id}" }
+ let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
+ let(:body_active) do
+ {
+ id: 1,
+ regcode: '631dc51f',
+ name: 'Subscription 1',
+ type: 'FULL',
+ status: 'ACTIVE',
+ starts_at: 'null',
+ expires_at: '2014-03-14T13:10:21.164Z',
+ system_limit: 6,
+ systems_count: 1,
+ service: {
+ product: {
+ id: system.activations.first.product.id
+ }
+ }
+ }
+ end
+
+ before do
+ allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double)
+
+ allow(plugin_double).to(
+ receive(:instance_valid?).and_return(true)
+ )
+ allow(File).to receive(:join).and_call_original
+ allow(InstanceVerification).to receive(:update_cache)
+ allow(ZypperAuth).to receive(:verify_instance).and_call_original
+ stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_active].to_json, headers: {})
+ headers['X-Instance-Data'] = 'IMDS'
+ end
+
+ context 'no registry' do
+ it 'refreshes registry cache key only' do
+ FileUtils.mkdir_p('repo/cache')
+ expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, system.activations.first.product.id)
+ get '/connect/systems/activations', headers: headers
+ FileUtils.rm_rf('repo/cache')
+ data = JSON.parse(response.body)
+ expect(SccProxy).not_to receive(:product_path_access)
+ expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud})
+ end
+ end
+
+ context 'registry' do
+ it 'refreshes registry cache key only' do
+ FileUtils.mkdir_p('repo/cache')
+ FileUtils.touch(cache_name)
+ expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, nil, registry: true)
+ get '/connect/systems/activations', headers: headers
+ FileUtils.rm_rf('repo/cache')
+ data = JSON.parse(response.body)
+ expect(SccProxy).not_to receive(:product_path_access)
+ expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud})
+ end
+ end
+ end
end
end
diff --git a/engines/scc_proxy/lib/scc_proxy/engine.rb b/engines/scc_proxy/lib/scc_proxy/engine.rb
index 08e5cae1f..47e6557e8 100644
--- a/engines/scc_proxy/lib/scc_proxy/engine.rb
+++ b/engines/scc_proxy/lib/scc_proxy/engine.rb
@@ -2,7 +2,7 @@
require 'net/http'
ANNOUNCE_URL = 'https://scc.suse.com/connect/subscriptions/systems'.freeze
-ACTIVATE_PRODUCT_URL = 'https://scc.suse.com/connect/systems/products'.freeze
+SYSTEM_PRODUCTS_URL = 'https://scc.suse.com/connect/systems/products'.freeze
SYSTEMS_ACTIVATIONS_URL = 'https://scc.suse.com/connect/systems/activations'.freeze
DEREGISTER_SYSTEM_URL = 'https://scc.suse.com/connect/systems'.freeze
DEREGISTER_PRODUCT_URL = 'https://scc.suse.com/connect/systems/products'.freeze
@@ -25,12 +25,6 @@
Net::HTTPRetriableError
].freeze
-INSTANCE_ID_KEYS = {
- amazon: 'instanceId',
- google: 'instance_id',
- microsoft: 'vmId'
-}.freeze
-
# rubocop:disable Metrics/ModuleLength
module SccProxy
class << self
@@ -43,7 +37,12 @@ class << self
# rubocop:disable ThreadSafety/InstanceVariableInClassMethod
def headers(auth, params)
@instance_id = if params && params.class != String
- get_instance_id(params)
+ InstanceVerification.provider.new(
+ nil,
+ nil,
+ nil,
+ params['instance_data']
+ ).instance_identifier
else
# if it is not JSON, it is the system_token already
# announce system has metadata
@@ -61,18 +60,6 @@ def headers(auth, params)
end
# rubocop:enable ThreadSafety/InstanceVariableInClassMethod
- def get_instance_id(params)
- verification_provider = InstanceVerification.provider.new(
- nil,
- nil,
- nil,
- params['instance_data']
- )
- instance_id_key = INSTANCE_ID_KEYS[params['hwinfo']['cloud_provider'].downcase.to_sym]
- iid = verification_provider.parse_instance_data
- iid[instance_id_key]
- end
-
def prepare_scc_announce_request(uri_path, auth, params)
scc_request = Net::HTTP::Post.new(uri_path, headers(auth, params))
@@ -117,6 +104,17 @@ def prepare_scc_request(uri_path, product, auth, params, mode)
scc_request
end
+ def prepare_scc_upgrade_request(uri_path, product, auth, mode)
+ scc_request = Net::HTTP::Put.new(uri_path, headers(auth, nil))
+ scc_request.body = {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ byos_mode: mode
+ }.to_json
+ scc_request
+ end
+
def announce_system_scc(auth, params)
uri = URI.parse(ANNOUNCE_URL)
http = Net::HTTP.new(uri.host, uri.port)
@@ -128,15 +126,26 @@ def announce_system_scc(auth, params)
JSON.parse(response.body)
end
- def scc_activate_product(product, auth, params, mode)
- uri = URI.parse(ACTIVATE_PRODUCT_URL)
+ def scc_activate_product(system, product, auth, params, mode)
+ uri = URI.parse(SYSTEM_PRODUCTS_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
scc_request = prepare_scc_request(uri.path, product, auth, params, mode)
- http.request(scc_request)
+ response = http.request(scc_request)
+ unless response.code_type == Net::HTTPCreated
+ error = JSON.parse(response.body)
+ Rails.logger.info "Could not activate #{product.product_string}, error: #{error['error']} #{response.code}"
+ error['error'] = SccProxy.parse_error(error['error']) if error['error'].include? 'json'
+ # if trying to activate first product on a hybrid system
+ # it means the system was "just" announced on this call
+ # if product activation failed, system should get de-register from SCC
+ SccProxy.deregister_system_scc(auth, system) if system.payg?
+
+ raise ActionController::TranslatedError.new(error['error'])
+ end
end
- def deactivate_product_scc(auth, product, params)
+ def deactivate_product_scc(auth, product, params, logger)
uri = URI.parse(DEREGISTER_PRODUCT_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
@@ -147,15 +156,29 @@ def deactivate_product_scc(auth, product, params)
arch: product.arch,
byos: true
}.to_json
- http.request(scc_request)
+ response = http.request(scc_request)
+ unless response.code_type == Net::HTTPOK
+ error = JSON.parse(response.body)
+ error['error'] = SccProxy.parse_error(error['error'], params[:token], params[:email]) if error['error'].include? 'json'
+ logger.info "Could not de-activate product '#{product.friendly_name}', error: #{error['error']} #{response.code}"
+ raise ActionController::TranslatedError.new(error['error'])
+ end
+ response
end
- def deregister_system_scc(auth, system_token)
+ def deregister_system_scc(auth, system)
uri = URI.parse(DEREGISTER_SYSTEM_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
- scc_request = Net::HTTP::Delete.new(uri.path, headers(auth, system_token))
- http.request(scc_request)
+ scc_request = Net::HTTP::Delete.new(uri.path, headers(auth, system.system_token))
+ response = http.request(scc_request)
+ unless response.code_type == Net::HTTPNoContent
+ error = JSON.parse(response.body)
+ Rails.logger.info "Could not de-activate system #{system.login}, error: #{error['error']} #{response.code}"
+ error['error'] = SccProxy.parse_error(error['error'], params[:token], params[:email]) if error['error'].include? 'json'
+ raise ActionController::TranslatedError.new(error['error'])
+ end
+ Rails.logger.info 'System successfully deregistered from SCC'
end
def parse_error(error_message, token = nil, email = nil)
@@ -165,14 +188,19 @@ def parse_error(error_message, token = nil, email = nil)
error_message
end
- def get_scc_activations(headers, system_token, mode)
+ def get_scc_activations(headers, system)
auth = headers['HTTP_AUTHORIZATION'] if headers && headers.include?('HTTP_AUTHORIZATION')
uri = URI.parse(SYSTEMS_ACTIVATIONS_URL)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
- uri.query = URI.encode_www_form({ byos_mode: mode })
- scc_request = Net::HTTP::Get.new(uri.path, headers(auth, system_token))
- http.request(scc_request)
+ uri.query = URI.encode_www_form({ byos_mode: system.proxy_byos_mode })
+ scc_request = Net::HTTP::Get.new(uri.path, headers(auth, system.system_token))
+ response = http.request(scc_request)
+ unless response.code_type == Net::HTTPOK
+ Rails.logger.info "Could not get the system (#{system.login}) activations, error: #{response.message} #{response.code}"
+ raise ActionController::TranslatedError.new(response.body)
+ end
+ JSON.parse(response.body)
end
def product_path_access(x_original_uri, products_ids)
@@ -200,11 +228,17 @@ def product_class_access(scc_systems_activations, product)
end
end
+ # rubocop:disable Metrics/PerceivedComplexity
def activations_fail_state(scc_systems_activations, headers, product = nil)
return SccProxy.product_class_access(scc_systems_activations, product) unless product.nil?
active_products_ids = scc_systems_activations.map { |act| act['service']['product']['id'] if act['status'].casecmp('active').zero? }.flatten
x_original_uri = headers.fetch('X-Original-URI', '')
+ # if there is no product info to compare the activations with
+ # probably means the query is to refresh credentials
+ # in any case, verification is true if ALL activations are ACTIVE
+ return { is_active: (scc_systems_activations.length == active_products_ids.length) } if x_original_uri.empty?
+
if SccProxy.product_path_access(x_original_uri, active_products_ids)
{ is_active: true }
else
@@ -229,15 +263,10 @@ def activations_fail_state(scc_systems_activations, headers, product = nil)
end
end
end
+ # rubocop:enable Metrics/PerceivedComplexity
- def scc_check_subscription_expiration(headers, login, system_token, logger, mode, product = nil) # rubocop:disable Metrics/ParameterLists
- response = SccProxy.get_scc_activations(headers, system_token, mode)
- unless response.code_type == Net::HTTPOK
- logger.info "Could not get the system (#{login}) activations, error: #{response.message} #{response.code}"
- response.message = SccProxy.parse_error(response.message) if response.message.include? 'json'
- return { is_active: false, message: response.message }
- end
- scc_systems_activations = JSON.parse(response.body)
+ def scc_check_subscription_expiration(headers, system, product = nil)
+ scc_systems_activations = SccProxy.get_scc_activations(headers, system)
return { is_active: false, message: 'No activations.' } if scc_systems_activations.empty?
no_status_products_ids = scc_systems_activations.map do |act|
@@ -246,6 +275,22 @@ def scc_check_subscription_expiration(headers, login, system_token, logger, mode
return { is_active: true } unless no_status_products_ids.all?(&:nil?)
SccProxy.activations_fail_state(scc_systems_activations, headers, product)
+ rescue StandardError
+ { is_active: false, message: 'Could not check the activations from SCC' }
+ end
+
+ def scc_upgrade(auth, product, system_login, mode, logger)
+ uri = URI.parse(SYSTEM_PRODUCTS_URL)
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = true
+ scc_request = prepare_scc_upgrade_request(uri.path, product, auth, mode)
+ response = http.request(scc_request)
+ unless response.code_type == Net::HTTPCreated
+ logger.info "Could not upgrade the system (#{system_login}), error: #{response.message} #{response.code}"
+ response.message = SccProxy.parse_error(response.message) if response.message.include? 'json'
+ raise ActionController::TranslatedError.new(response.body)
+ end
+ response
end
end
@@ -261,11 +306,16 @@ def announce_system
auth_header = nil
auth_header = request.headers['HTTP_AUTHORIZATION'] if request.headers.include?('HTTP_AUTHORIZATION')
system_information = hwinfo_params[:hwinfo].to_json
-
+ instance_data = params.fetch(:instance_data, nil)
if has_no_regcode?(auth_header)
# no token sent to check with SCC
# standard announce case
- @system = System.create!(hostname: params[:hostname], system_information: system_information, proxy_byos_mode: :payg)
+ @system = System.create!(
+ hostname: params[:hostname],
+ system_information: system_information,
+ proxy_byos_mode: :payg,
+ instance_data: instance_data
+ )
else
request.request_parameters['proxy_byos_mode'] = 'byos'
response = SccProxy.announce_system_scc(auth_header, request.request_parameters)
@@ -275,7 +325,9 @@ def announce_system
password: response['password'],
hostname: params[:hostname],
proxy_byos_mode: :byos,
- system_information: system_information
+ proxy_byos: true,
+ system_information: system_information,
+ instance_data: instance_data
)
end
logger.info("System '#{@system.hostname}' announced")
@@ -303,55 +355,26 @@ def has_no_regcode?(auth_header)
Api::Connect::V3::Systems::ProductsController.class_eval do
before_action :scc_activate_product, only: %i[activate]
+ before_action :scc_upgrade, only: %i[upgrade], if: -> { @system.byos? }
protected
- # rubocop:disable Metrics/PerceivedComplexity
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/AbcSize
def scc_activate_product
- logger.info "Activating product #{@product.product_string} to SCC"
- auth = nil
- auth = request.headers['HTTP_AUTHORIZATION'] if request.headers.include?('HTTP_AUTHORIZATION')
- mode = nil
- if @system.byos?
- mode = 'byos'
- elsif !@product.free? && @product.extension? && params[:token].present?
- mode = 'hybrid'
- # the extensions must be the same version and arch
- # than base product
- base_prod = @system.products.find_by(product_type: :base)
- if @system.payg? && base_prod.present?
- raise 'Incompatible extension product' unless @product.arch == base_prod.arch && @product.version == base_prod.version
-
- params['hostname'] = @system.hostname
- params['proxy_byos_mode'] = mode
- params['scc_login'] = @system.login
- params['scc_password'] = @system.password
- params['hwinfo'] = JSON.parse(@system.system_information)
- params['hwinfo']['instance_data'] = @system.instance_data
- announce_auth = "Token token=#{params[:token]}"
-
- response = SccProxy.announce_system_scc(announce_auth, params)
- end
+ product_hash = @product.attributes.symbolize_keys.slice(:identifier, :version, :arch)
+ unless InstanceVerification.provider.new(logger, request, product_hash, @system.instance_data).allowed_extension?
+ error = ActionController::TranslatedError.new(N_('Product not supported for this instance'))
+ error.status = :forbidden
+ raise error
end
-
+ mode = find_mode
unless mode.nil?
# if system is byos or hybrid and there is a token
# make a request to SCC
- response = SccProxy.scc_activate_product(@product, auth, params, mode)
- unless response.code_type == Net::HTTPCreated
- error = JSON.parse(response.body)
- logger.info "Could not activate #{@product.product_string}, error: #{error['error']} #{response.code}"
- error['error'] = SccProxy.parse_error(error['error']) if error['error'].include? 'json'
- if @system.payg?
- # if trying to activate first product on a hybrid system
- # it means the system was "just" announced on this call
- # if product activation failed, system should get de-register from SCC
- deregister_hybrid(request.headers['HTTP_AUTHORIZATION'])
- end
- raise ActionController::TranslatedError.new(error['error'])
- end
+ logger.info "Activating product #{@product.product_string} to SCC"
+ logger.info 'No token provided' if params[:token].blank?
+ SccProxy.scc_activate_product(
+ @system, @product, request.headers['HTTP_AUTHORIZATION'], params, mode
+ )
# if the system is PAYG and the registration code is valid for the extension,
# then the system is hybrid
# update the system to HYBRID mode if HYBRID MODE and system not HYBRID already
@@ -360,23 +383,51 @@ def scc_activate_product
logger.info "Product #{@product.product_string} successfully activated with SCC"
InstanceVerification.update_cache(request.remote_ip, @system.login, @product.id)
end
- logger.info 'No token provided' if params[:token].blank?
end
- def deregister_hybrid(auth)
- response = SccProxy.deregister_system_scc(auth, @system.system_token)
- unless response.code_type == Net::HTTPNoContent
- error = JSON.parse(response.body)
- logger.info "Could not de-activate system #{@system.login}, error: #{error['error']} #{response.code}"
- error['error'] = SccProxy.parse_error(error['error'], params[:token], params[:email]) if error['error'].include? 'json'
- raise ActionController::TranslatedError.new(error['error'])
+ def find_mode
+ if @system.byos?
+ 'byos'
+ elsif !@product.free? && @product.extension? && params[:token].present?
+ announce_base_product_hybrid 'hybrid'
+ 'hybrid'
+ end
+ end
+
+ def announce_base_product_hybrid(mode)
+ # in order for SCC to be able activate the extension (i.e. LTSS)
+ # the system must be announced to SCC first
+ base_prod = @system.products.find_by(product_type: :base)
+ # the extensions must be the same version and arch
+ # than base product
+ if @system.payg? && base_prod.present?
+ raise 'Incompatible extension product' unless @product.arch == base_prod.arch && @product.version == base_prod.version
+
+ update_params_system_info mode
+ SccProxy.announce_system_scc(
+ "Token token=#{params[:token]}", params
+ )
end
- logger.info 'System successfully deregistered from SCC'
+ end
+
+ def scc_upgrade
+ logger.info "Upgrading system to product #{@product.product_string} to SCC"
+ auth = nil
+ auth = request.headers['HTTP_AUTHORIZATION'] if request.headers.include?('HTTP_AUTHORIZATION')
+ mode = 'byos' if @system.byos?
+ SccProxy.scc_upgrade(auth, @product, @system.login, mode, logger)
+ logger.info "System #{@system.login} successfully upgraded with SCC"
+ end
+
+ def update_params_system_info(mode)
+ params['hostname'] = @system.hostname
+ params['proxy_byos_mode'] = mode
+ params['scc_login'] = @system.login
+ params['scc_password'] = @system.password
+ params['hwinfo'] = JSON.parse(@system.system_information)
+ params['instance_data'] = @system.instance_data
end
end
- # rubocop:enable Metrics/AbcSize
- # rubocop:enable Metrics/CyclomaticComplexity
- # rubocop:enable Metrics/PerceivedComplexity
Api::Connect::V4::Systems::ProductsController.class_eval do
before_action :scc_deactivate_product, only: %i[destroy]
@@ -386,42 +437,28 @@ def deregister_hybrid(auth)
def scc_deactivate_product
auth = request.headers['HTTP_AUTHORIZATION']
if @system.byos? && @product[:product_type] != 'base'
- response = SccProxy.deactivate_product_scc(auth, @product, @system.system_token)
- handle_response(response)
+ SccProxy.deactivate_product_scc(auth, @product, @system.system_token, logger)
elsif @system.hybrid? && @product.extension?
# check if product is on SCC and
# if it is -> de-activate it
- scc_systems_activations = find_hybrid_product_on_scc(request.headers)
- result = SccProxy.product_class_access(scc_systems_activations, @product.product_class)
- if result[:is_active] || (!result[:is_active] && result[:message].downcase.include?('expired'))
- # if product is active on SCC or
- # product subscription expired
+ scc_hybrid_system_activations = SccProxy.get_scc_activations(headers, @system)
+ if scc_hybrid_system_activations.map { |act| act['service']['product']['id'] == @product.id }.present?
+ # if product is found on SCC, regardless of the state
# it is OK to remove it from SCC
- response = SccProxy.deactivate_product_scc(auth, @product, @system.system_token)
- handle_response(response)
- elsif result[:message].downcase.include?('unexpected error')
- raise ActionController::TranslatedError.new(result[:message])
+ SccProxy.deactivate_product_scc(auth, @product, @system.system_token, logger)
+ make_system_payg(auth) if scc_hybrid_system_activations.reject { |act| act['service']['product']['id'] == @product.id }.blank?
end
end
logger.info "Product '#{@product.friendly_name}' successfully deactivated from SCC"
end
- def find_hybrid_product_on_scc(headers)
- response = SccProxy.get_scc_activations(headers, @system.system_token, @system.proxy_byos_mode)
- unless response.code_type == Net::HTTPOK
- logger.info "Could not get the system (#{@system.login}) activations, error: #{response.message} #{response.code}"
- raise ActionController::TranslatedError.new(response.body)
- end
- JSON.parse(response.body)
- end
-
- def handle_response(response)
- unless response.code_type == Net::HTTPOK
- error = JSON.parse(response.body)
- error['error'] = SccProxy.parse_error(error['error'], params[:token], params[:email]) if error['error'].include? 'json'
- logger.info "Could not de-activate product '#{@product.friendly_name}', error: #{error['error']} #{response.code}"
- raise ActionController::TranslatedError.new(error['error'])
- end
+ def make_system_payg(auth)
+ # if the system does not have more products activated on SCC
+ # switch it back to payg
+ # drop the just de-activated activation from the list to avoid another call to SCC
+ # and check if there is any product
+ SccProxy.deregister_system_scc(auth, @system)
+ @system.payg!
end
end
@@ -433,15 +470,7 @@ def handle_response(response)
def scc_deregistration
if @system.byos? || @system.hybrid?
# byos and hybrid systems should get de-register from SCC
- auth = request.headers['HTTP_AUTHORIZATION']
- response = SccProxy.deregister_system_scc(auth, @system.system_token)
- unless response.code_type == Net::HTTPNoContent
- error = JSON.parse(response.body)
- logger.info "Could not de-activate system #{@system.login}, error: #{error['error']} #{response.code}"
- error['error'] = SccProxy.parse_error(error['error'], params[:token], params[:email]) if error['error'].include? 'json'
- raise ActionController::TranslatedError.new(error['error'])
- end
- logger.info 'System successfully deregistered from SCC'
+ SccProxy.deregister_system_scc(request.headers['HTTP_AUTHORIZATION'], @system)
end
end
end
diff --git a/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb b/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb
index 9daced829..f023a15b9 100644
--- a/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb
+++ b/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb
@@ -354,7 +354,7 @@
end
let(:product) do
FactoryBot.create(
- :product, :product_sles, :extension, :with_mirrored_repositories, :with_mirrored_extensions,
+ :product, :product_sles_ltss, :extension, :with_mirrored_repositories, :with_mirrored_extensions,
base_products: [system_payg.products.first]
)
end
@@ -415,7 +415,120 @@
allow(File).to receive(:directory?)
allow(FileUtils).to receive(:mkdir_p)
allow(FileUtils).to receive(:touch)
+ allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double)
+ allow(plugin_double).to receive(:allowed_extension?).and_return(true)
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_payg.login}-#{product.id}"
+ )
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_payg.login}"
+ )
+ allow(plugin_double).to receive(:instance_valid?).and_return(true)
+ end
+
+ context 'when LTSS not allowed' do
+ before do
+ allow(plugin_double).to receive(:allowed_extension?).and_return(false)
+ end
+
+ it 'raises an error' do
+ stub_request(:post, scc_register_system_url)
+ .to_return(status: 403, body: { ok: 'OK' }.to_json, headers: {})
+
+ post url, params: payload, headers: headers
+ data = JSON.parse(response.body)
+ expect(data['error']).to include('Product not supported for this instance')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when system has hw info' do
+ let(:instance_data) { '{"instanceId": "dummy_instance_data"}' }
+ let(:new_system_token) { 'BBBBBBBB-BBBB-4BBB-9BBB-BBBBBBBBBBBB' }
+ let(:serialized_service_json) do
+ V3::ServiceSerializer.new(
+ product.service,
+ base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s
+ ).to_json
+ end
+
+ let(:serialized_service_sap_json) do
+ V3::ServiceSerializer.new(
+ product_sap.service,
+ base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s
+ ).to_json
+ end
+
+ context 'when system is connected to SCC' do
+ let(:system_payg) do
+ FactoryBot.create(:system, :payg, :with_system_information, :with_activated_base_product, instance_data: instance_data,
+ system_token: new_system_token)
+ end
+ let(:product) do
+ FactoryBot.create(
+ :product, :product_sles_ltss, :extension, :with_mirrored_repositories, :with_mirrored_extensions,
+ base_products: [system_payg.products.first]
+ )
+ end
+ let(:subscription_response) do
+ {
+ id: 4206714,
+ regcode: 'bar',
+ name: 'SUSE Employee subscription for SUSE Linux Enterprise Server for SAP Applications',
+ type: 'internal',
+ status: 'ACTIVE',
+ starts_at: '2019-03-20T09:48:52.658Z',
+ expires_at: '2024-03-20T09:48:52.658Z',
+ system_limit: '100',
+ systems_count: '156',
+ virtual_count: nil,
+ product_classes: [
+ 'AiO',
+ '7261',
+ 'SLE-HAE-X86',
+ '7261-BETA',
+ 'SLE-HAE-X86-BETA',
+ 'AiO-BETA',
+ '7261-ALPHA',
+ 'SLE-HAE-X86-ALPHA',
+ 'AiO-ALPHA'
+ ],
+ product_ids: [
+ 1959,
+ 1421
+ ],
+ skus: [],
+ systems: [
+ {
+ id: 3021957,
+ login: 'SCC_foo',
+ password: '5ee7273ac6ac4d7f',
+ last_seen_at: '2019-03-20T14:01:05.424Z'
+ }
+ ]
+ }
+ end
+
+ before do
+ allow(plugin_double).to(
+ receive(:instance_valid?)
+ .and_raise(InstanceVerification::Exception, 'Custom plugin error')
+ )
+ end
+ context 'with a valid registration code' do
+ before do
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 201,
+ body: { id: 'bar' }.to_json,
+ headers: {}
+ )
+ allow(File).to receive(:directory?)
+ allow(FileUtils).to receive(:mkdir_p)
+ allow(FileUtils).to receive(:touch)
allow(InstanceVerification).to receive(:write_cache_file).twice.with(
Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_payg.login}-#{product.id}"
)
@@ -474,7 +587,10 @@
end
context 'when de-register system from SCC fails' do
+ let(:error_message) { "Could not activate #{product.product_string}, error: No product found on SCC for: foo bar x86_64 json api 401" }
+
before do
+ allow(Rails.logger).to receive(:info)
stub_request(:delete, scc_systems_url)
.to_return(
status: 401,
@@ -483,11 +599,11 @@
)
stub_request(:post, scc_register_system_url)
.to_return(status: 201, body: { ok: 'OK' }.to_json, headers: {})
-
- post url, params: payload, headers: headers
end
it 'renders an error with exception details' do
+ expect(Rails.logger).to receive(:info).with(error_message)
+ post url, params: payload, headers: headers
data = JSON.parse(response.body)
expect(data['error']).to eq('Could not de-register system')
end
diff --git a/engines/scc_proxy/spec/requests/api/connect/v4/systems/products_controller_spec.rb b/engines/scc_proxy/spec/requests/api/connect/v4/systems/products_controller_spec.rb
index 4a38985e5..8c1801c33 100644
--- a/engines/scc_proxy/spec/requests/api/connect/v4/systems/products_controller_spec.rb
+++ b/engines/scc_proxy/spec/requests/api/connect/v4/systems/products_controller_spec.rb
@@ -187,7 +187,7 @@
end
end
- context 'when SCC API returns an error' do
+ context 'when SCC API suceeds for HYBRID system' do
let(:product) do
FactoryBot.create(:product, :product_sles, :extension, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_hybrid)
end
@@ -204,200 +204,123 @@
base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s
).to_json
end
- let(:body_active) do
- {
- id: 1,
- regcode: '631dc51f',
- name: 'Subscription 1',
- type: 'FULL',
- status: 'ACTIVE',
- starts_at: 'null',
- expires_at: DateTime.parse((Time.zone.today + 1).to_s),
- system_limit: 6,
- systems_count: 1,
- service: {
- product: {
- id: system_hybrid.activations.first.product.id,
- product_class: system_hybrid.activations.first.product.product_class
- }
- }
- }
- end
let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
+ let(:scc_systems_url) { 'https://scc.suse.com/connect/systems' }
before do
stub_request(:delete, scc_systems_products_url)
.to_return(
- status: 422,
+ status: 200,
body: "{\"error\": \"Could not de-activate product \'#{product.friendly_name}\'\"}",
headers: {}
)
- stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_active].to_json, headers: {})
- delete url, params: payload, headers: headers
- end
-
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Could not de-activate product \'SUSE Linux Enterprise Server 15 SP3 x86_64\'')
- end
- end
-
- context 'when product is expired' do
- let(:product) do
- FactoryBot.create(:product, :product_sles, :extension, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_hybrid)
- end
- let(:payload) do
- {
- identifier: product.identifier,
- version: product.version,
- arch: product.arch
- }
- end
- let(:serialized_service_json) do
- V3::ServiceSerializer.new(
- product.service,
- base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s
- ).to_json
- end
- let(:body_expired) do
- {
- id: 1,
- regcode: '631dc51f',
- name: 'Subscription 1',
- type: 'FULL',
- status: 'EXPIRED',
- starts_at: 'null',
- expires_at: DateTime.parse((Time.zone.today - 1).to_s),
- system_limit: 6,
- systems_count: 1,
- service: {
- product: {
- id: system_hybrid.activations.first.product.id,
- product_class: system_hybrid.activations.first.product.product_class
+ stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: body_active, headers: {})
+ end
+
+ context 'when only one product was active' do
+ let(:body_active) do
+ [{
+ id: 1,
+ regcode: '631dc51f',
+ name: 'Subscription 1',
+ type: 'FULL',
+ status: 'ACTIVE',
+ starts_at: 'null',
+ expires_at: DateTime.parse((Time.zone.today + 1).to_s),
+ system_limit: 6,
+ systems_count: 1,
+ service: {
+ product: {
+ id: system_hybrid.activations.first.product.id,
+ product_class: system_hybrid.activations.first.product.product_class
+ }
}
- }
- }
- end
- let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
-
- before do
- stub_request(:delete, scc_systems_products_url)
- .to_return(
- status: 422,
- body: "{\"error\": \"Could not de-activate product \'#{product.friendly_name}\'\"}",
- headers: {}
- )
- stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_expired].to_json, headers: {})
- delete url, params: payload, headers: headers
- end
-
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Could not de-activate product \'SUSE Linux Enterprise Server 15 SP3 x86_64\'')
- end
- end
-
- context 'when product is not active' do
- let(:product) do
- FactoryBot.create(:product, :product_sles, :extension, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_hybrid)
- end
- let(:payload) do
- {
- identifier: product.identifier,
- version: product.version,
- arch: product.arch
- }
- end
- let(:serialized_service_json) do
- V3::ServiceSerializer.new(
- product.service,
- base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s
- ).to_json
- end
- let(:body_not_activated) do
- {
- id: 1,
- regcode: '631dc51f',
- name: 'Subscription 1',
- type: 'FULL',
- status: 'ACTIVE',
- starts_at: 'null',
- expires_at: DateTime.parse((Time.zone.today - 1).to_s),
- system_limit: 6,
- systems_count: 1,
- service: {
- product: {
+ }].to_json
+ end
+
+ context 'when deactivating the system succeeds' do
+ before do
+ stub_request(:delete, scc_systems_url).to_return(status: 204, body: '', headers: {})
+ delete url, params: payload, headers: headers
+ end
+
+ it 'makes the hybrid system payg' do
+ updated_system = System.find_by(login: system_hybrid.login)
+ expect(updated_system.payg?).to eq(true)
+ end
+ end
+
+ context 'when deactivating the system fails' do
+ before do
+ allow(Rails.logger).to receive(:info)
+ stub_request(:delete, scc_systems_url).to_return(
+ status: 422,
+ body: '{"error": "Oh oh, something went wrong"}',
+ headers: {}
+ )
+ delete url, params: payload, headers: headers
+ end
+
+ it 'makes the hybrid system payg' do
+ expect(Rails.logger).to(
+ have_received(:info).with(
+ "Could not de-activate system #{system_hybrid.login}, error: Oh oh, something went wrong 422"
+ ).once
+ )
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Oh oh, something went wrong')
+ end
+ end
+ end
+
+ context 'when more activations are left' do
+ let(:body_active) do
+ [
+ {
id: 1,
- product_class: product.product_class + 'FOO'
- }
- }
- }
- end
- let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
-
- before do
- stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_not_activated].to_json, headers: {})
- delete url, params: payload, headers: headers
- end
-
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq(nil)
- end
- end
-
- context 'when product has unknown status' do
- let(:product) do
- FactoryBot.create(:product, :product_sles, :extension, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_hybrid)
- end
- let(:payload) do
- {
- identifier: product.identifier,
- version: product.version,
- arch: product.arch
- }
- end
- let(:serialized_service_json) do
- V3::ServiceSerializer.new(
- product.service,
- base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s
- ).to_json
- end
- let(:body_unknown_status) do
- {
- id: 1,
- regcode: '631dc51f',
- name: 'Subscription 1',
- type: 'FULL',
- status: 'FOO',
- starts_at: 'null',
- expires_at: DateTime.parse((Time.zone.today - 1).to_s),
- system_limit: 6,
- systems_count: 1,
- service: {
- product: {
- id: system_hybrid.activations.first.product.id,
- product_class: system_hybrid.activations.first.product.product_class + 'FOO'
+ regcode: '631dc51f',
+ name: 'Subscription 1',
+ type: 'FULL',
+ status: 'ACTIVE',
+ starts_at: 'null',
+ expires_at: DateTime.parse((Time.zone.today + 1).to_s),
+ system_limit: 6,
+ systems_count: 1,
+ service: {
+ product: {
+ id: system_hybrid.activations.first.product.id,
+ product_class: system_hybrid.activations.first.product.product_class
+ }
+ }
+ }, {
+ id: 2,
+ regcode: '631dc51f',
+ name: 'Subscription 1',
+ type: 'FULL',
+ status: 'ACTIVE',
+ starts_at: 'null',
+ expires_at: DateTime.parse((Time.zone.today + 1).to_s),
+ system_limit: 6,
+ systems_count: 1,
+ service: {
+ product: {
+ id: '30',
+ product_class: '23'
+ }
+ }
}
- }
- }
- end
- let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
-
- before do
- stub_request(:delete, scc_systems_products_url)
- .to_return(
- status: 422,
- body: "{\"error\": \"Could not de-activate product \'#{product.friendly_name}\'\"}",
- headers: {}
- )
- stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_unknown_status].to_json, headers: {})
- delete url, params: payload, headers: headers
- end
-
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Unexpected error when checking product subscription.')
+ ].to_json
+ end
+
+ before do
+ stub_request(:delete, scc_systems_url).to_return(status: 204, body: '', headers: {})
+ delete url, params: payload, headers: headers
+ end
+
+ it 'keeps the system as hybrid' do
+ updated_system = System.find_by(login: system_hybrid.login)
+ expect(updated_system.hybrid?).to eq(true)
+ end
end
end
end
diff --git a/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb b/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb
index 311c38fc0..b3fabfab6 100644
--- a/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb
+++ b/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb
@@ -7,27 +7,30 @@ class AuthenticationController < ::ApplicationController
# This is the endpoint for nginx subrequest auth check
def check
request_uri = request.headers['X-Original-URI']
- auth_result = path_allowed?(request.headers['X-Original-URI'])
+ auth_result = path_allowed?(request.headers)
logger.info "Authentication subrequest for #{request_uri} -- #{auth_result ? 'allowed' : 'denied'}"
head auth_result ? :ok : :forbidden
end
protected
- def path_allowed?(path)
+ def path_allowed?(headers)
+ path = headers['X-Original-URI']
return false if path.blank?
+
return true if path =~ %r{/product\.license/}
path = '/' + path.gsub(/^#{RMT::DEFAULT_MIRROR_URL_PREFIX}/, '')
-
# Allow access to SLES 12 and 12-SP1 repos for systems migrating from SLES 11
has_sles11 = @system.products.where(identifier: 'SUSE_SLES').first
return true if (has_sles11 && (path =~ %r{/12/} || path =~ %r{/12-SP1/}))
- all_allowed_paths.find { |allowed_path| path =~ /^#{Regexp.escape(allowed_path)}/ }
+ all_allowed_paths(headers).find { |allowed_path| path =~ /^#{Regexp.escape(allowed_path)}/ }
end
- def all_allowed_paths
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def all_allowed_paths(headers)
# return all versions of the same product and arch
# (that the system has available with that subscription)
# in order to validate access not only for current product but others
@@ -36,10 +39,31 @@ def all_allowed_paths
# to them or verify paths
all_product_versions = @system.products.map { |p| Product.where(identifier: p.identifier, arch: p.arch) }.flatten
allowed_paths = all_product_versions.map { |prod| prod.repositories.pluck(:local_path) }.flatten
+ # Allow SLE Micro to access all free SLES repositories
+ sle_micro = @system.products.any? { |p| p.identifier.downcase.include?('sle-micro') }
+ if sle_micro
+ system_products_archs = @system.products.pluck(:arch)
+ product_free_sles_modules_only = Product.where(
+ "(lower(identifier) like 'sle-module%' or lower(identifier) like 'packagehub')
+ and lower(identifier) not like '%sap%'
+ and arch = '#{system_products_archs.first}'
+ and free = 1"
+ )
+ end
+ same_arch = product_free_sles_modules_only.any? { |p| system_products_archs.include?(p.arch) } if product_free_sles_modules_only.present?
+ allowed_paths += product_free_sles_modules_only.map { |prod| prod.repositories.pluck(:local_path) }.flatten if same_arch
+
# for the SUMa PAYG offers, RMT access verification code allows access
# to the SUMa Client Tools channels and SUMa Proxy channels
# when product is SUMA_Server and PAYG or SUMA_Server and used as SCC proxy
- manager_prod = @system.products.any? { |p| p.identifier.downcase.include?('manager-server') }
+ manager_prod = @system.products.any? do |p|
+ manager = p.identifier.downcase.include?('manager-server')
+ # SUMA 5.0 must have access to SUMA 4.3, 4.2 and so on
+ micro = p.identifier.downcase.include?('sle-micro')
+ instance_id_header = headers.fetch('X-Instance-Identifier', '').casecmp('suse-manager-server').zero?
+ instance_version_header = headers.fetch('X-Instance-Version', '0').split('.')[0] >= '5'
+ manager || (micro && instance_id_header && instance_version_header)
+ end
if manager_prod
# add all SUMA products paths
@@ -49,5 +73,7 @@ def all_allowed_paths
end
allowed_paths
end
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/PerceivedComplexity
end
end
diff --git a/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb b/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb
index c921d3855..fcb5f4e03 100644
--- a/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb
+++ b/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb
@@ -5,7 +5,7 @@ module StrictAuthentication
RSpec.describe AuthenticationController, type: :request do
subject { response }
- let(:system) { FactoryBot.create(:system, :with_activated_product) }
+ let(:system) { FactoryBot.create(:system, :with_activated_product_sle_micro) }
describe '#check' do
context 'without authentication' do
@@ -39,6 +39,36 @@ module StrictAuthentication
its(:code) { is_expected.to eq '403' }
end
+ context 'when requesting a file in an activated SLES repo on a SLE Micro system' do
+ let(:free_product) do
+ prod = FactoryBot.create(
+ :product, :module, :with_mirrored_repositories
+ )
+ prod.identifier = 'sle-module-foo'
+ prod.arch = system.products.first.arch
+ prod.save!
+ prod
+ end
+ let(:requested_uri) { '/repo' + free_product.repositories.first[:local_path] + '/repodata/repomd.xml' }
+
+ its(:code) { is_expected.to eq '200' }
+ end
+
+ context 'when requesting a file in an activated SLES SAP repo on a SLE Micro system' do
+ let(:free_product) do
+ prod = FactoryBot.create(
+ :product, :module, :with_mirrored_repositories
+ )
+ prod.identifier = 'sle-module-foo-sap'
+ prod.arch = system.products.first.arch
+ prod.save!
+ prod
+ end
+ let(:requested_uri) { '/repo' + free_product.repositories.first[:local_path] + '/repodata/repomd.xml' }
+
+ its(:code) { is_expected.to eq '403' }
+ end
+
context 'when requesting a file in an activated repo' do
let(:requested_uri) { '/repo' + system.repositories.first[:local_path] + '/repodata/repomd.xml' }
diff --git a/engines/zypper_auth/lib/zypper_auth/engine.rb b/engines/zypper_auth/lib/zypper_auth/engine.rb
index db43ac907..8478ee7c1 100644
--- a/engines/zypper_auth/lib/zypper_auth/engine.rb
+++ b/engines/zypper_auth/lib/zypper_auth/engine.rb
@@ -6,7 +6,7 @@ def auth_logger
Thread.current[:logger]
end
- def verify_instance(request, logger, system)
+ def verify_instance(request, logger, system, params_product_id = nil)
return false unless request.headers['X-Instance-Data']
instance_data = Base64.decode64(request.headers['X-Instance-Data'].to_s)
@@ -31,12 +31,13 @@ def verify_instance(request, logger, system)
)
is_valid = verification_provider.instance_valid?
- return false if is_valid && system.hybrid? && !handle_scc_subscription(request, system, verification_provider, logger)
+ return false if is_valid && system.hybrid? && !handle_scc_subscription(request, system, verification_provider, params_product_id)
+
# update repository and registry cache
InstanceVerification.update_cache(request.remote_ip, system.login, base_product.id)
is_valid
rescue InstanceVerification::Exception => e
- return handle_scc_subscription(request, system, verification_provider, logger) if system.byos?
+ return handle_scc_subscription(request, system, verification_provider) if system.byos?
ZypperAuth.zypper_auth_message(request, system, verification_provider, e.message)
false
@@ -49,8 +50,9 @@ def verify_instance(request, logger, system)
false
end
- def handle_scc_subscription(request, system, verification_provider, logger)
- result = SccProxy.scc_check_subscription_expiration(request.headers, system.login, system.system_token, logger, system.proxy_byos_mode)
+ def handle_scc_subscription(request, system, verification_provider, params_product_id = nil)
+ product_class = Product.find_by(id: params_product_id).product_class if params_product_id.present?
+ result = SccProxy.scc_check_subscription_expiration(request.headers, system, product_class)
return true if result[:is_active]
ZypperAuth.zypper_auth_message(request, system, verification_provider, result[:message])
@@ -128,7 +130,7 @@ def make_repo_url(base_url, repo_local_path, service_name = nil)
# additional validation for zypper service XML controller
before_action :verify_instance
def verify_instance
- unless ZypperAuth.verify_instance(request, logger, @system)
+ unless ZypperAuth.verify_instance(request, logger, @system, params.fetch('id', nil))
render(xml: { error: 'Instance verification failed' }, status: 403)
end
end
diff --git a/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb b/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb
index 643fe3f6f..f0ab69371 100644
--- a/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb
+++ b/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb
@@ -245,21 +245,16 @@
before do
stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_active].to_json, headers: {})
- # allow(SccProxy).to receive(:get_scc_activations).and_return(status: 200, body: [body_active].to_json, headers: {})
expect(URI).to receive(:encode_www_form).with({ byos_mode: 'hybrid' })
allow(File).to receive(:directory?).and_return(true)
allow(Dir).to receive(:mkdir)
allow(FileUtils).to receive(:touch)
- # get '/api/auth/check', headers: headers
end
it 'returns true' do
result = SccProxy.scc_check_subscription_expiration(
headers,
- system_hybrid.login,
- system_hybrid.system_token,
- nil,
- system_hybrid.proxy_byos_mode,
+ system_hybrid,
system_hybrid.activations.first.product.product_class + '-LTSS'
)
expect(result[:is_active]).to be(true)
@@ -298,10 +293,7 @@
it 'returns false, expired' do
result = SccProxy.scc_check_subscription_expiration(
headers,
- system_hybrid.login,
- system_hybrid.system_token,
- nil,
- system_hybrid.proxy_byos_mode,
+ system_hybrid,
system_hybrid.activations.first.product.product_class + '-LTSS'
)
expect(result[:is_active]).to eq(false)
@@ -341,10 +333,7 @@
it 'returns product not activated' do
result = SccProxy.scc_check_subscription_expiration(
headers,
- system_hybrid.login,
- system_hybrid.system_token,
- nil,
- system_hybrid.proxy_byos_mode,
+ system_hybrid,
system_hybrid.activations.first.product.product_class + '-LTSS'
)
expect(result[:is_active]).to eq(false)
@@ -384,10 +373,7 @@
it 'returns unexpected error' do
result = SccProxy.scc_check_subscription_expiration(
headers,
- system_hybrid.login,
- system_hybrid.system_token,
- nil,
- system_hybrid.proxy_byos_mode,
+ system_hybrid,
system_hybrid.activations.first.product.product_class + '-LTSS'
)
expect(result[:is_active]).to eq(false)
diff --git a/lib/rmt/config.rb b/lib/rmt/config.rb
index 650a316ad..4c7ebe27c 100644
--- a/lib/rmt/config.rb
+++ b/lib/rmt/config.rb
@@ -1,3 +1,4 @@
+# :nocov:
require 'config'
require_relative '../rmt'
@@ -77,3 +78,4 @@ def validate_int(value)
end
end
end
+# :nocov:
diff --git a/package/obs/rmt-server.changes b/package/obs/rmt-server.changes
index 71ad51f29..c84e3dbf6 100644
--- a/package/obs/rmt-server.changes
+++ b/package/obs/rmt-server.changes
@@ -1,3 +1,19 @@
+-------------------------------------------------------------------
+Wed Oct 30 09:01:32 UTC 2024 - Natnael Getahun
+
+- Version 2.20 (unreleased)
+ * RMT packaging: don't overwrite custom sync/mirror timer config on package update
+ * Extend column size for repository and file paths (bsc#1229152)
+ * Forward suseconnect client user-agents to SCC
+ * rmt-server-pubcloud:
+ * Fix LTSS product verification (bsc#1230154)
+ * Fix activations check when no product info is available (bsc#1230157)
+ * Fix Azure SCC connection (bsc#1233314)
+ * Deny access of Azure Basic type images to LTSS
+ * Allow SLE Micro system to access SLES repositories (bsc#1230419)
+ * Skip system token rotation in read-only APIs
+ * Enable RMT to handle the new dl.suse.com CDN domain (bsc#1234641)
+
-------------------------------------------------------------------
Wed Aug 21 15:28:43 UTC 2024 - Jesús Bermúdez Velázquez
diff --git a/package/obs/rmt-server.spec b/package/obs/rmt-server.spec
index 6aa6b9a2f..5e357fbbf 100644
--- a/package/obs/rmt-server.spec
+++ b/package/obs/rmt-server.spec
@@ -269,6 +269,7 @@ chrpath -d %{buildroot}%{lib_dir}/vendor/bundle/ruby/*/extensions/*/*/mysql2-*/m
%dir %{_sysconfdir}/slp.reg.d
%config(noreplace) %attr(0640, %{rmt_user}, root) %{_sysconfdir}/rmt.conf
%config(noreplace) %{_sysconfdir}/slp.reg.d/rmt-server.reg
+
%{_mandir}/man8/rmt-cli.8%{?ext_man}
%{_bindir}/rmt-cli
%{_bindir}/rmt-data-import
@@ -289,6 +290,11 @@ chrpath -d %{buildroot}%{lib_dir}/vendor/bundle/ruby/*/extensions/*/*/mysql2-*/m
%{_unitdir}/rmt-server-systems-scc-sync.timer
%{_unitdir}/rmt-uptime-cleanup.service
%{_unitdir}/rmt-uptime-cleanup.timer
+%config(noreplace) %{_unitdir}/rmt-server-mirror.timer
+%config(noreplace) %{_unitdir}/rmt-server-sync.timer
+%config(noreplace) %{_unitdir}/rmt-server-systems-scc-sync.timer
+%config(noreplace) %{_unitdir}/rmt-uptime-cleanup.timer
+
%dir %{_datadir}/bash-completion/
%dir %{_datadir}/bash-completion/completions/
%{_datadir}/bash-completion/completions/rmt-cli
@@ -323,6 +329,8 @@ chrpath -d %{buildroot}%{lib_dir}/vendor/bundle/ruby/*/extensions/*/*/mysql2-*/m
%{_unitdir}/rmt-server-regsharing.timer
%{_unitdir}/rmt-server-trim-cache.service
%{_unitdir}/rmt-server-trim-cache.timer
+%config(noreplace) %{_unitdir}/rmt-server-regsharing.timer
+%config(noreplace) %{_unitdir}/rmt-server-trim-cache.timer
%pre
getent group %{rmt_group} >/dev/null || %{_sbindir}/groupadd -r %{rmt_group}
diff --git a/package/obs/rmt.conf b/package/obs/rmt.conf
index c2f410e24..a1d6eca60 100644
--- a/package/obs/rmt.conf
+++ b/package/obs/rmt.conf
@@ -12,7 +12,10 @@ scc:
username:
password:
sync_systems: true
-
+ metrics:
+ enabled: false
+ job_name: rmt
+
mirroring:
mirror_src: false
dedup_method: hardlink
diff --git a/spec/factories/products.rb b/spec/factories/products.rb
index 30532e1fc..4612dfb00 100644
--- a/spec/factories/products.rb
+++ b/spec/factories/products.rb
@@ -57,6 +57,23 @@
friendly_version { '15 SP3' }
end
+ trait :product_sles_ltss do
+ identifier { 'SLES-LTSS' }
+ name { 'SUSE Linux Enterprise Server LTSS' }
+ description { 'SUSE Linux Enterprise offers a comprehensive suite of products...' }
+ shortname { 'SLES15-SP3-LTSS' }
+ former_identifier { 'SLES_LTSS' }
+ product_type { 'extension' }
+ product_class { 'LTSS' }
+ release_type { nil }
+ release_stage { 'released' }
+ version { '15.3' }
+ arch { 'x86_64' }
+ free { false }
+ cpe { 'cpe:/o:suse:sles:15:sp3' }
+ friendly_version { '15 SP3' }
+ end
+
trait :product_sles_sap do
identifier { 'SLES_SAP' }
name { 'SUSE Linux Enterprise Server' }
@@ -73,6 +90,22 @@
friendly_version { '15 SP3' }
end
+ trait :product_sle_micro do
+ identifier { 'SLE-Micro' }
+ name { 'SUSE Linux Enterprise Server' }
+ description { 'SUSE Linux Enterprise offers a comprehensive suite of products...' }
+ shortname { 'SLES15-SP6' }
+ former_identifier { 'SUSE_SLES_MICRO' }
+ product_type { :base }
+ release_type { nil }
+ release_stage { 'released' }
+ version { '15.6' }
+ arch { 'x86_64' }
+ free { false }
+ cpe { 'cpe:/o:suse:sles_sap:15:sp6' }
+ friendly_version { '15 SP6' }
+ end
+
trait :extension do
product_type { 'extension' }
end
diff --git a/spec/factories/systems.rb b/spec/factories/systems.rb
index 8fb81aa38..ceab7d02e 100644
--- a/spec/factories/systems.rb
+++ b/spec/factories/systems.rb
@@ -57,6 +57,17 @@
end
end
+ trait :with_activated_product_sle_micro do
+ transient do
+ product { create(:product, :product_sle_micro, :with_mirrored_repositories) }
+ subscription { nil }
+ end
+
+ after :create do |system, evaluator|
+ create(:activation, system: system, service: evaluator.product.service, subscription: evaluator.subscription)
+ end
+ end
+
trait :with_system_information do
system_information do
{
diff --git a/spec/lib/rmt/cli/smt_importer_spec.rb b/spec/lib/rmt/cli/smt_importer_spec.rb
index d77404a52..17063bbc0 100644
--- a/spec/lib/rmt/cli/smt_importer_spec.rb
+++ b/spec/lib/rmt/cli/smt_importer_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
require 'rmt/cli/smt_importer'
-describe RMT::CLI::SMTImporter do
+describe RMT::CLI::SMTImporter, :skip_sqlite do
let(:data_dir) { File.join(Dir.pwd, 'spec/fixtures/files/csv') }
let(:no_systems) { false }
let(:importer) { described_class.new(data_dir, no_systems) }
diff --git a/spec/lib/rmt/lockfile_spec.rb b/spec/lib/rmt/lockfile_spec.rb
index d1b86f968..c06644bc3 100644
--- a/spec/lib/rmt/lockfile_spec.rb
+++ b/spec/lib/rmt/lockfile_spec.rb
@@ -3,7 +3,7 @@
RSpec.describe RMT::Lockfile do
let(:lock_name) { nil }
- describe '#lock' do
+ describe '#lock', :skip_sqlite do
subject(:lock) { described_class.lock(lock_name) { nil } }
context 'with an unnamed lock' do
diff --git a/spec/lib/rmt/scc_spec.rb b/spec/lib/rmt/scc_spec.rb
index 802cf1c8a..f1e3b9d61 100644
--- a/spec/lib/rmt/scc_spec.rb
+++ b/spec/lib/rmt/scc_spec.rb
@@ -238,7 +238,7 @@
described_class.new.sync
end
- it 'removes existing predecessor associations' do
+ it 'removes existing predecessor associations', :skip_sqlite do
expect { ProductPredecessorAssociation.find(existing_association.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
@@ -261,7 +261,18 @@
:repository,
:with_products,
auth_token: nil,
- external_url: 'https://example.com/repos/not/updates.suse.com/'
+ external_url: 'https://installer-updates.suse.com/repos/not/updates',
+ installer_updates: true
+ )
+ end
+ let!(:custom_repo) do
+ FactoryBot.create(
+ :repository,
+ :with_products,
+ auth_token: nil,
+ external_url: 'http://customer.com/stuff.suse.com/x86',
+ installer_updates: false,
+ scc_id: nil
)
end
@@ -297,6 +308,10 @@ def scc
it 'other repos without auth_tokens persist' do
expect { other_repo_without_token.reload }.not_to raise_error
end
+
+ it 'custom repos without auth_tokens persist' do
+ expect { custom_repo.reload }.not_to raise_error
+ end
end
describe '#export' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 3c958ac15..3c49cd6fa 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -146,7 +146,7 @@
expect(friendly_id).to eq('my-repo-100000')
end
- it 'appends with unicode within the chain' do
+ it 'appends with unicode within the chain', :skip_sqlite do
create(:repository, friendly_id: 'my-repo')
create(:repository, friendly_id: 'my-repö-1')
expect(friendly_id).to eq('my-repo-2')
@@ -165,13 +165,13 @@
expect(friendly_id).to eq('dümmy-repö')
end
- it 'appends with unicode within the chain' do
+ it 'appends with unicode within the chain', :skip_sqlite do
create(:repository, friendly_id: 'dummy-repo')
create(:repository, friendly_id: 'dümmy-repö-1')
expect(friendly_id).to eq('dümmy-repö-2')
end
- it 'correctly appends' do
+ it 'correctly appends', :skip_sqlite do
create(:repository, friendly_id: 'dummy-repo')
create(:repository, friendly_id: 'dummy-repo-1')
expect(friendly_id).to eq('dümmy-repö-2')
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 5d71fea9f..9c8bffe57 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -58,6 +58,12 @@
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
+
+ # Skipping some tests when running with (experimental) sqlite backend.
+ # Some tests / code parts are using specific mysql behavior
+ if ActiveRecord::Base.connection.adapter_name == 'SQLite'
+ config.filter_run_excluding skip_sqlite: true
+ end
end
Shoulda::Matchers.configure do |config|
diff --git a/spec/requests/api/connect/base_controller_spec.rb b/spec/requests/api/connect/base_controller_spec.rb
index 70ef9aaff..8832909ea 100644
--- a/spec/requests/api/connect/base_controller_spec.rb
+++ b/spec/requests/api/connect/base_controller_spec.rb
@@ -55,15 +55,6 @@ def require_product
end
end
- shared_examples 'updates the system token' do
- it 'updates the system token' do
- allow(SecureRandom).to receive(:uuid).and_return(new_system_token)
-
- expect { get :service, params: { id: 1 } }
- .to change { system.reload.system_token }
- .from(current_system_token).to(new_system_token)
- end
- end
shared_examples "does not update the old system's token" do
it 'does not update the system token' do
@@ -74,8 +65,6 @@ def require_product
shared_examples 'creates a duplicate system' do
it 'creates a new System (duplicate)' do
- allow(SecureRandom).to receive(:uuid).and_return(new_system_token)
-
expect { get :service, params: { id: 1 } }
.to change { System.get_by_credentials(system.login, system.password).count }
.by(1)
@@ -85,7 +74,6 @@ def require_product
expect(duplicate_system).not_to eq(system)
expect(duplicate_system.activations.count).to eq(system.activations.count)
expect(duplicate_system.system_token).not_to eq(system.system_token)
- expect(duplicate_system.system_token).to eq(new_system_token)
end
end
@@ -182,8 +170,7 @@ def require_product
let(:system) { create(:system, hostname: 'system') }
include_examples 'does not create a duplicate system'
- include_examples 'updates the system token'
- include_examples 'responds with a new token'
+ include_examples "does not update the old system's token"
end
context 'when the system has a token and the header matches it' do
@@ -193,8 +180,7 @@ def require_product
let(:system) { create(:system, hostname: 'system', system_token: current_system_token) }
include_examples 'does not create a duplicate system'
- include_examples 'updates the system token'
- include_examples 'responds with a new token'
+ include_examples "does not update the old system's token"
end
context 'when the system has a token and the header is blank' do
@@ -208,7 +194,6 @@ def require_product
include_examples "does not update the old system's token"
include_examples 'creates a duplicate system'
- include_examples 'responds with a new token'
end
context 'when the system has a token and the header does not match it' do
diff --git a/spec/requests/api/connect/v3/systems/activations_controller_spec.rb b/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
index a4ff0a562..6a62ee2bd 100644
--- a/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
+++ b/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
@@ -69,5 +69,28 @@
expect(system.scc_synced_at).to be_nil
end
end
+
+ context 'system token header' do
+ context 'when system token header is present in request' do
+ let(:token_headers) do
+ authenticated_headers.merge({ 'System-Token' => 'some_token' })
+ end
+
+ it 'sets system token in response headers' do
+ get url, headers: token_headers
+ expect(response.code).to eq '200'
+ expect(response.headers).to include('System-Token')
+ expect(response.headers['System-Token']).not_to be_nil
+ expect(response.headers['System-Token']).not_to be_empty
+ end
+
+ it 'does not set system token header if no system token header in request' do
+ get url, headers: authenticated_headers
+
+ expect(response.code).to eq '200'
+ expect(response.headers).not_to include('System-Token')
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/connect/v3/systems/products_controller_spec.rb b/spec/requests/api/connect/v3/systems/products_controller_spec.rb
index 60c1bbb0d..9f6c47916 100644
--- a/spec/requests/api/connect/v3/systems/products_controller_spec.rb
+++ b/spec/requests/api/connect/v3/systems/products_controller_spec.rb
@@ -135,6 +135,26 @@
end
end
+ shared_context 'activate with token in request headers' do
+ let(:payload) do
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ token: regcode
+ }
+ end
+
+ before { post url, headers: { 'System-Token' => 'existing_token' }.merge(headers), params: payload }
+ subject do
+ Struct.new(:body, :code, :headers).new(
+ JSON.parse(response.body, symbolize_names: true),
+ response.status,
+ response.headers
+ )
+ end
+ end
+
context 'unknown subscription' do
include_context 'with subscriptions'
let(:regcode) { 'NOT-EXISTING-SUBSCRIPTION' }
@@ -173,6 +193,17 @@
expect(activation.product).to eq(product)
end
end
+
+ context 'token update after activation is success' do
+ let(:subscription) { create :subscription, :with_products }
+ let(:product) { subscription.products.first }
+ let(:regcode) { subscription.regcode }
+
+ include_context 'activate with token in request headers'
+ its(:code) { is_expected.to eq(201) }
+ its(:headers) { is_expected.to include('System-Token') }
+ its(:headers['System-Token']) { is_expected.not_to eq('existing_token') }
+ end
end
end
@@ -225,6 +256,18 @@
its(:body) { is_expected.to eq(serialized_json) }
end
+ describe 'response header should contain token' do
+ subject { response }
+
+ let(:token_headers) do
+ headers.merge({ 'System-Token' => 'some_token' })
+ end
+
+ before { get url, headers: token_headers, params: payload }
+ its(:code) { is_expected.to eq('200') }
+ its(:headers) { is_expected.to include('System-Token') }
+ end
+
describe 'response with "-" in version' do
subject { response }
@@ -246,6 +289,33 @@
end
end
+ context 'when SLE Micro product is activated' do
+ let(:system) { FactoryBot.create(:system, :with_activated_product_sle_micro) }
+ let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories) }
+ let(:payload) do
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: system.products.first.arch
+ }
+ end
+ let(:serialized_json) do
+ V3::ProductSerializer.new(
+ product,
+ base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s
+ ).to_json
+ end
+
+ describe 'response' do
+ subject { response }
+
+ before { get url, headers: headers, params: payload }
+
+ its(:code) { is_expected.to eq('200') }
+ its(:body) { is_expected.to eq(serialized_json) }
+ end
+ end
+
context 'with eula_url' do
subject { response }
@@ -312,6 +382,18 @@
system.reload
end
+ it 'calls refresh_system_token after upgrade action when system token header is present' do
+ put url, headers: headers.merge('System-Token' => 'test_token'), params: payload
+ expect(response.code).to eq('201')
+ expect(response.headers).to include('System-Token')
+ expect(response.headers['System-Token']).not_to eq('test_token')
+ end
+
+ it 'No update in token after upgrade action when system token header is absent' do
+ put url, headers: headers, params: payload
+ expect(response.code).to eq('201')
+ expect(response.headers).not_to include('System-Token')
+ end
context 'new product' do
its(:code) { is_expected.to eq('201') }
its(:body) { is_expected.to eq(serialized_json) }
diff --git a/spec/requests/api/connect/v3/systems/systems_controller_spec.rb b/spec/requests/api/connect/v3/systems/systems_controller_spec.rb
index 14cb0f07e..91f73d4f4 100644
--- a/spec/requests/api/connect/v3/systems/systems_controller_spec.rb
+++ b/spec/requests/api/connect/v3/systems/systems_controller_spec.rb
@@ -3,6 +3,8 @@
RSpec.describe Api::Connect::V3::Systems::SystemsController do
include_context 'auth header', :system, :login, :password
include_context 'version header', 3
+ include_context 'user-agent header'
+ include_context 'zypp user-agent header'
let(:system) { FactoryBot.create(:system, hostname: 'initial') }
let(:url) { '/connect/systems' }
@@ -105,6 +107,43 @@
expect(system.reload.hostname).to be_nil
end
end
+
+ context 'stores client\'s user-agent' do
+ let(:headers) { auth_header.merge(user_agent_header) }
+
+ it 'stores suseconnect version' do
+ update_action
+ expect(system.reload.system_information_hash[:user_agent]).to eq('suseconnect-ng/1.2')
+ end
+ end
+
+ context 'doesn\'t store zypp user-agent' do
+ let(:headers) { auth_header.merge(zypp_user_agent_header) }
+
+ it 'ignores zypp user-agent' do
+ update_action
+ expect(system.reload.system_information_hash[:user_agent]).to be_nil
+ end
+ end
+
+ context 'response header should contain token' do
+ let(:headers) { auth_header.merge('System-Token': 'existing-token') }
+
+ it 'contains refreshed token in response' do
+ update_action
+ expect(response.headers).to include('System-Token')
+ expect(response.headers['System-Token']).not_to equal('existing-token')
+ end
+ end
+
+ context 'response header should not contain token' do
+ let(:headers) { auth_header }
+
+ it 'contains refreshed token in response' do
+ update_action
+ expect(response.headers).not_to include('System-Token')
+ end
+ end
end
describe '#deregister' do
diff --git a/spec/requests/api/connect/v4/systems/products_controller_spec.rb b/spec/requests/api/connect/v4/systems/products_controller_spec.rb
index 8bf9a64f4..6f960a941 100644
--- a/spec/requests/api/connect/v4/systems/products_controller_spec.rb
+++ b/spec/requests/api/connect/v4/systems/products_controller_spec.rb
@@ -140,4 +140,77 @@
end
end
end
+
+ describe 'system token refresh' do
+ let(:system) { FactoryBot.create(:system, :with_activated_base_product) }
+ let(:headers) { auth_header.merge(version_header).merge('System-Token' => 'existing_token') }
+
+ context 'token refresh for destroy action' do
+ let(:product) { FactoryBot.create(:product, :extension, :with_mirrored_repositories, :activated, system: system) }
+ let(:payload) { { identifier: product.identifier, version: product.version, arch: product.arch } }
+
+ it 'refreshes system token when System-Token header is present' do
+ delete connect_systems_products_url,
+ headers: headers,
+ params: payload
+
+ expect(response.status).to eq(200)
+ expect(response.headers).to include('System-Token')
+ expect(response.headers['System-Token']).not_to eq('existing_token')
+ end
+
+ it 'does not refresh token when System-Token header is absent' do
+ headers_without_token = auth_header.merge(version_header)
+ expect_any_instance_of(described_class).not_to receive(:refresh_system_token)
+
+ delete connect_systems_products_url,
+ headers: headers_without_token,
+ params: payload
+
+ expect(response.status).to eq(200)
+ expect(response.headers).not_to include('System-Token')
+ end
+ end
+
+ context 'token refresh for synchronize action' do
+ let(:path) { '/connect/systems/products/synchronize' }
+
+ it 'refreshes system token when System-Token header is present' do
+ params = system.products.map do |product|
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ release_type: product.release_type
+ }
+ end
+ post path,
+ params: { products: params },
+ headers: headers
+
+ expect(response.status).to eq(200)
+ expect(response.headers).to include('System-Token')
+ expect(response.headers['System-Token']).not_to eq('existing_token')
+ end
+
+ it 'does not refresh token when System-Token header is absent' do
+ headers_without_token = auth_header.merge(version_header)
+
+ params = system.products.map do |product|
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ release_type: product.release_type
+ }
+ end
+ post path,
+ params: { products: params },
+ headers: headers_without_token
+
+ expect(response.status).to eq(200)
+ expect(response.headers).not_to include('System-Token')
+ end
+ end
+ end
end
diff --git a/spec/services/repository_service_spec.rb b/spec/services/repository_service_spec.rb
index 1387ea3b0..0035395b5 100644
--- a/spec/services/repository_service_spec.rb
+++ b/spec/services/repository_service_spec.rb
@@ -88,7 +88,7 @@
it('is not custom') { expect(repository.custom?).to eq(true) }
- context 'already existing repositories with changing URL' do
+ context 'already existing repositories with changing URL', :skip_sqlite do
subject(:repository) do
service.create_repository!(product, url, attributes, custom: custom).reload
url = 'https://foo.bar.com/bar/foo'
diff --git a/spec/support/shared_contexts/headers.rb b/spec/support/shared_contexts/headers.rb
index 352363776..f1946a453 100644
--- a/spec/support/shared_contexts/headers.rb
+++ b/spec/support/shared_contexts/headers.rb
@@ -2,6 +2,14 @@
let(:version_header) { { 'Accept' => "application/vnd.scc.suse.com.v#{version}+json" } }
end
+shared_context 'user-agent header' do |_version|
+ let(:user_agent_header) { { 'User-Agent' => 'suseconnect-ng/1.2' } }
+end
+
+shared_context 'zypp user-agent header' do |_version|
+ let(:zypp_user_agent_header) { { 'User-Agent' => 'zypp/1.0' } }
+end
+
shared_context 'auth header' do |login_object, login_method, password_method|
let(:auth_header) do
basic_auth_header send(login_object).send(login_method), send(login_object).send(password_method)