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/pull_request_template.md b/.github/pull_request_template.md
index 0af224699..e1b09f1cd 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,8 +1,14 @@
## Description
-Please describe your change and provide as much context as possible.
+
-Fixes # (issue)
+* Related Issue / Ticket / Trello card:
+
+## How to test
+
+
## Change Type
@@ -16,13 +22,13 @@ Fixes # (issue)
*Please check off each item if the requirement is met.*
-- [ ] I have verified that my code follows RMT's coding standards with `rubocop`.
- [ ] I have reviewed my own code and believe that it's ready for an external review.
- [ ] I have provided comments for any hard-to-understand code.
- [ ] I have documented the `MANUAL.md` file with any changes to the user experience.
-- [ ] RMT's test coverage remains at 100%.
- [ ] If my changes are non-trivial, I have added a changelog entry to notify users at `package/obs/rmt-server.changes`.
-## Other Notes
+## Review
+
+Please check out our [review guidelines](https://github.com/SUSE/scc-docs/blob/master/team/workflow/code_review.md)
+and get in touch with the author to get a shared understanding of the change.
-Please use this space to provide notes or thoughts to the team, such as tips on how to review/demo your changes.
diff --git a/.github/workflows/lint-unit.yml b/.github/workflows/lint-unit.yml
index 66bf3f77c..e97009ec4 100644
--- a/.github/workflows/lint-unit.yml
+++ b/.github/workflows/lint-unit.yml
@@ -77,17 +77,17 @@ jobs:
- name: Prepare database
run: |
- bundle exec rails db:migrate
+ bundle exec rails db:drop db:create db:migrate
- 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 rspec --format documentation
-
+ bundle exec rake test:core
+
- name: Run PubCloud engines tests
run: |
bundle exec rake test:engines
@@ -96,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 82f2f5ec0..d57185c60 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 b0ee3b50a..1ca660f8c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -30,13 +30,15 @@ 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)
base64 (0.2.0)
- bigdecimal (3.1.6)
+ bigdecimal (3.1.8)
builder (3.2.4)
byebug (11.1.3)
bzip2-ffi (1.1.1)
@@ -104,7 +106,8 @@ GEM
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
fakefs (1.8.0)
- fast_gettext (2.3.0)
+ fast_gettext (2.4.0)
+ prime
ffaker (2.21.0)
ffi (1.16.3)
formatador (0.2.5)
@@ -118,7 +121,7 @@ GEM
prime
racc
text (>= 1.3.0)
- gettext_i18n_rails (1.12.0)
+ gettext_i18n_rails (1.13.0)
fast_gettext (>= 0.9.0)
gettext_test_log (0.2.1)
guard (2.16.2)
@@ -135,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.1)
+ jwt (2.9.3)
base64
listen (3.6.0)
rb-fsevent (~> 0.10, >= 0.10.3)
@@ -151,7 +154,7 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
lumberjack (1.2.8)
- memory_profiler (1.0.1)
+ memory_profiler (1.0.2)
method_source (1.1.0)
mini_portile2 (2.6.1)
minitest (5.15.0)
@@ -159,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)
@@ -172,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)
@@ -208,8 +213,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
- rexml (3.2.8)
- strscan (>= 3.0.9)
+ rexml (3.3.9)
ronn (0.7.3)
hpricot (>= 0.8.2)
mustache (>= 0.7.0)
@@ -266,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_parser (3.19.2)
sexp_processor (~> 4.16)
@@ -295,7 +300,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)
@@ -303,7 +307,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
text (1.3.1)
thor (1.2.2)
- timecop (0.9.9)
+ timecop (0.9.10)
tins (1.26.0)
sync
typhoeus (1.4.1)
@@ -316,10 +320,27 @@ GEM
activesupport (>= 3)
railties (>= 3)
yard (~> 0.9.20)
- webmock (3.23.0)
+ 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)
@@ -350,7 +371,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)
@@ -381,6 +402,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 7412e8e59..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:
@@ -41,14 +41,6 @@ Note: Look below for direction on publishing to registry.
```
2. On github, submit a release for the tag. See https://help.github.com/en/articles/creating-releases for assistance.
-#### Helm chart update process
-
-RMT helm chart is found [here](https://github.com/SUSE/helm-charts.git).
-
-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.
-
-[Bruno Leon](mailto:bruno.leon@suse.com) is the maintainer and point of contact for rmt-helm.
-
#### Submit Requests to openSUSE Factory and SLES
To get a maintenance request accepted, each changelog entry needs to have at
@@ -72,13 +64,7 @@ osc sr systemsmanagement:SCC:RMT rmt-server openSUSE:Factory
###### Get target codestreams where to submit
-To check out which codestreams the package is currently maintained in, run:
-
-```bash
-osc -A https://api.suse.de maintained rmt-server
-```
-
-For a more detailed view which target codestreams are in which state, check out: [Codestream overview](https://maintenance.suse.de/maintained/?package=rmt-server)
+To check out which codestreams RMT is currently maintained, see https://smelt.suse.de/maintained/?q=rmt-server.
###### Submit updates
@@ -112,8 +98,15 @@ You can check the status of your requests [here](https://build.opensuse.org/pack
After your requests have been accepted, they still have to pass maintenance testing before they are released to customers. You can check their progress at [maintenance.suse.de](https://maintenance.suse.de/search/?q=rmt-server). If you still need help, the maintenance team can be reached at [maint-coord@suse.de](maint-coord@suse.de) or #maintenance on irc.suse.de.
-## Packaging and publishing to SUSE registry
+## Container image and publishing to SUSE registry
+
+SUSE registry houses the rmt-server docker image. The image is built automatically on OBS/IBS, and can be found [here](https://build.opensuse.org/package/show/devel:BCI:SLE-15-SP6/rmt-server-image). It is getting published here: `registry.suse.com/suse/rmt-server`.
+
-SUSE registry houses the rmt-server docker image. The image is built on OBS/IBS, project for SLES 15sp4 based distributions can be found [here](https://build.opensuse.org/package/show/devel:BCI:SLE-15-SP4/rmt-server-image).
+#### Helm chart update process
+
+RMT helm chart is defined [here](https://github.com/SUSE/helm-charts.git) and published at `registry.suse.com/suse/rmt-helm`.
+
+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.
-At the moment of writing, the publishing process has to be done manually. This can be achieved by reaching out to the Auto-Build team (only available internally).
+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/v3/systems/products_controller.rb b/app/controllers/api/connect/v3/systems/products_controller.rb
index b985a5d48..5688cc9b4 100644
--- a/app/controllers/api/connect/v3/systems/products_controller.rb
+++ b/app/controllers/api/connect/v3/systems/products_controller.rb
@@ -12,7 +12,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,
@@ -108,7 +114,7 @@ def create_product_activation
# in BYOS mode, we rely on the activation being performed in
# `engines/scc_proxy/lib/scc_proxy/engine.rb` and don't need further checks here
- return activation if @system.proxy_byos
+ return activation if @system.byos?
if @subscription.present?
activation.subscription = @subscription
@@ -123,8 +129,10 @@ def remove_previous_product_activations(product_ids)
end
def load_subscription
- # Find subscription by regcode if provided, otherwise use the first subscription (bsc#1220109)
- if params[:token].present? && !@system.proxy_byos
+ # Find subscription by regcode if provided and not a public cloud system,
+ # otherwise check if there's already an activation with a matching
+ # subscription (for migrations, bsc#1220109)
+ if params[:token].present? && @system.not_applicable?
@subscription = Subscription.find_by(regcode: params[:token])
unless @subscription
raise ActionController::TranslatedError.new(N_('No subscription with this Registration Code found'))
diff --git a/app/controllers/api/connect/v3/systems/systems_controller.rb b/app/controllers/api/connect/v3/systems/systems_controller.rb
index b7622e252..863557279 100644
--- a/app/controllers/api/connect/v3/systems/systems_controller.rb
+++ b/app/controllers/api/connect/v3/systems/systems_controller.rb
@@ -21,8 +21,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/application_controller.rb b/app/controllers/application_controller.rb
index 12eaadef0..686972895 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -18,6 +18,7 @@ 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.
@@ -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/system.rb b/app/models/system.rb
index 0bfa987d4..2d9f97808 100644
--- a/app/models/system.rb
+++ b/app/models/system.rb
@@ -1,4 +1,11 @@
class System < ApplicationRecord
+ # This value has meaning/relevance only used in public cloud scenarios
+ # This value indicates that the system is using
+ # NOT_APPLICABLE (systems outside the public cloud),
+ # PAYG (pay as you go),
+ # BYOS (bring your own subscription) or
+ # a mix of both (hybrid).
+ enum proxy_byos_mode: { not_applicable: 0, payg: 1, byos: 2, hybrid: 3 }
after_initialize :init
@@ -30,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/bin/rmt-cli b/bin/rmt-cli
index b71c9ceb6..1e649f8b8 100755
--- a/bin/rmt-cli
+++ b/bin/rmt-cli
@@ -42,6 +42,12 @@ if Settings.try(:host_system).blank? && File.exist?(RMT::CREDENTIALS_FILE_LOCATI
warn "Run as root or adjust the permissions."
end
+unless Settings['database']
+ warn "Error loading database config."
+ warn 'Please make sure that /etc/rmt.conf is readable by the rmt process and has your database configured.'
+ exit RMT::CLI::Error::ERROR_OTHER
+end
+
db_config = RMT::Config.db_config
ActiveRecord::Base.establish_connection(db_config)
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..811e44ff2 100644
--- a/config/rmt.yml
+++ b/config/rmt.yml
@@ -24,6 +24,9 @@ scc:
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/20240711154147_add_index_to_systems_system_token.rb b/db/migrate/20240711154147_add_index_to_systems_system_token.rb
new file mode 100644
index 000000000..27b56ad24
--- /dev/null
+++ b/db/migrate/20240711154147_add_index_to_systems_system_token.rb
@@ -0,0 +1,5 @@
+class AddIndexToSystemsSystemToken < ActiveRecord::Migration[6.1]
+ def change
+ add_index :systems, %i[system_token]
+ end
+end
diff --git a/db/migrate/20240729103525_update_proxy_byos_column_type.rb b/db/migrate/20240729103525_update_proxy_byos_column_type.rb
new file mode 100644
index 000000000..50cbf85fd
--- /dev/null
+++ b/db/migrate/20240729103525_update_proxy_byos_column_type.rb
@@ -0,0 +1,11 @@
+class UpdateProxyByosColumnType < ActiveRecord::Migration[6.1]
+ def up
+ add_column :systems, :proxy_byos_mode, :integer, default: 0
+ System.where(proxy_byos: false).where.not(instance_data: nil).in_batches.update_all proxy_byos_mode: :payg
+ System.where(proxy_byos: true).in_batches.update_all proxy_byos_mode: :byos
+ end
+
+ def down
+ remove_column :systems, :proxy_byos_mode
+ end
+end
diff --git a/db/migrate/20240821114908_change_local_path_type.rb b/db/migrate/20240821114908_change_local_path_type.rb
new file mode 100644
index 000000000..5c2f53cce
--- /dev/null
+++ b/db/migrate/20240821114908_change_local_path_type.rb
@@ -0,0 +1,13 @@
+class ChangeLocalPathType < ActiveRecord::Migration[6.1]
+ def up
+ safety_assured do
+ change_column :repositories, :local_path, :string, limit: 512
+ change_column :downloaded_files, :local_path, :string, limit: 512
+ end
+ end
+
+ def down
+ change_column :repositories, :local_path, :string
+ change_column :downloaded_files, :local_path, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6f9f2a30a..1584f1814 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2024_01_29_140413) do
+ActiveRecord::Schema.define(version: 2024_08_21_114908) do
create_table "activations", charset: "utf8", force: :cascade do |t|
t.bigint "service_id", null: false
@@ -34,7 +34,7 @@
create_table "downloaded_files", charset: "utf8", force: :cascade do |t|
t.string "checksum_type"
t.string "checksum"
- t.string "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.string "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
@@ -169,11 +169,13 @@
t.datetime "scc_registered_at"
t.bigint "scc_system_id", comment: "System ID in SCC (if the system registration was forwarded; needed for forwarding de-registrations)"
t.boolean "proxy_byos", default: false
+ t.integer "proxy_byos_mode", default: 0
t.string "system_token"
t.text "system_information", size: :long
t.text "instance_data"
t.index ["login", "password", "system_token"], name: "index_systems_on_login_and_password_and_system_token", unique: true
t.index ["login", "password"], name: "index_systems_on_login_and_password"
+ t.index ["system_token"], name: "index_systems_on_system_token"
t.check_constraint "json_valid(`system_information`)", name: "system_information"
end
diff --git a/engines/instance_verification/lib/instance_verification/engine.rb b/engines/instance_verification/lib/instance_verification/engine.rb
index 77d52c621..45438e3f0 100644
--- a/engines/instance_verification/lib/instance_verification/engine.rb
+++ b/engines/instance_verification/lib/instance_verification/engine.rb
@@ -58,11 +58,11 @@ def verify_product_activation
if product.base?
verify_base_product_activation(product)
- else
- verify_extension_activation(product)
+ elsif !product.free? && params[:token].blank?
+ verify_payg_extension_activation!(product)
end
rescue InstanceVerification::Exception => e
- unless @system.proxy_byos
+ unless @system.byos?
# BYOS instances that use RMT as a proxy are expected to fail the
# instance verification check, however, PAYG instances may send registration
# code, as such, instance verification engine checks for those BYOS
@@ -79,11 +79,10 @@ def verify_product_activation
raise ActionController::TranslatedError.new('Unexpected instance verification error has occurred')
end
- def verify_extension_activation(product)
+ def verify_payg_extension_activation!(product)
return if product.free?
base_product = @system.products.find_by(product_type: :base)
-
subscription = Subscription.joins(:product_classes).find_by(
subscription_product_classes: {
product_class: base_product.product_class
@@ -91,7 +90,7 @@ def verify_extension_activation(product)
)
# This error would occur only if there's a problem with subscription setup on SCC side
- raise "Can't find a subscription for base product #{base_product.product_string}" unless subscription
+ raise InstanceVerification::Exception, "Can't find a subscription for base product #{base_product.product_string}" unless subscription
allowed_product_classes = subscription.product_classes.pluck(:product_class)
@@ -100,6 +99,8 @@ def verify_extension_activation(product)
'The product is not available for this instance'
)
end
+ logger.info "Product #{@product.product_string} available for this instance"
+ InstanceVerification.update_cache(request.remote_ip, @system.login, product.id)
end
def verify_base_product_activation(product)
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 a8cc64bcc..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
@@ -1,5 +1,6 @@
require 'rails_helper'
+# rubocop:disable RSpec/NestedGroups
describe Api::Connect::V3::Systems::ProductsController, type: :request do
include_context 'auth header', :system, :login, :password
include_context 'version header', 3
@@ -8,7 +9,7 @@
let(:headers) { auth_header.merge(version_header) }
let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions) }
let(:product_sap) { FactoryBot.create(:product, :product_sles_sap, :with_mirrored_repositories, :with_mirrored_extensions) }
-
+ let(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
let(:payload) do
{
identifier: product.identifier,
@@ -20,200 +21,553 @@
describe '#activate' do
let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
- context "when system doesn't have hw_info" do
- let(:system) { FactoryBot.create(:system) }
-
- 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
- allow(File).to receive(:directory?)
- allow(Dir).to receive(:mkdir)
- allow(FileUtils).to receive(:touch)
- post url, params: payload, headers: headers
- end
- end
-
- context 'when system has hw_info' do
- let(:instance_data) { 'dummy_instance_data' }
- let(:system) { FactoryBot.create(:system, :with_system_information, instance_data: instance_data) }
- 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
+ after { FileUtils.rm_rf(File.dirname(Rails.application.config.registry_cache_dir)) }
- 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 the system is byos' do
+ context "when system doesn't have hw_info" do
+ let(:system) { FactoryBot.create(:system, :byos) }
- context 'when verification provider returns false' do
before do
+ stub_request(:post, 'https://scc.suse.com/connect/systems/products')
+ .to_return(
+ status: 201,
+ body: { ok: 'ok' }.to_json,
+ headers: {}
+ )
+ end
+
+ it 'class instance verification provider' 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)
+ .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)
post url, params: payload, headers: headers
end
+ end
- it 'renders an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ context 'when system has hw_info' do
+ let(:instance_data) { 'dummy_instance_data' }
+ let(:system) { FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data) }
+ 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
+
+ let(:serialized_service_byos_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 verification provider returns false' do
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+
+ before do
+ 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
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ end
+ end
+
+ context 'when verification provider raises an unhandled exception' do
+ before do
+ 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
+
+ it 'renders an error with exception details' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ end
end
end
+ end
- context 'when verification provider raises an unhandled exception' do
- before do
+ context 'when the system is payg' do
+ context "when system doesn't have hw_info" do
+ let(:system) { FactoryBot.create(:system, :payg) }
+
+ it 'class instance verification provider' 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')
+ .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)
post url, params: payload, headers: headers
end
+ end
- it 'renders an error with exception details' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ context 'when system has hw_info' do
+ let(:instance_data) { 'dummy_instance_data' }
+ let(:system) { FactoryBot.create(:system, :payg, :with_system_information, instance_data: instance_data) }
+ 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 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).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
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ end
+ end
+
+ 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).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
+
+ it 'renders an error with exception details' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ end
+ end
+
+ context 'when verification provider raises an instance verification exception' do
+ let(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
+
+ 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).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
+
+ it 'renders an error with exception details' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Instance verification failed: Custom plugin error')
+ end
end
end
- context 'when verification provider raises an instance verification exception' do
+ context 'when activating extensions with errors' do
+ let(:instance_data) { 'dummy_instance_data' }
+ let(:system) do
+ FactoryBot.create(
+ :system, :payg, :with_system_information, :with_activated_product, product: base_product, instance_data: instance_data
+ )
+ 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(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+ let(:payload) do
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ instance_data: 'dummy_instance_data',
+ proxy_byos_mode: :payg,
+ hwinfo:
+ {
+ hostname: 'test',
+ cpus: '1',
+ sockets: '1',
+ hypervisor: 'Xen',
+ arch: 'x86_64',
+ uuid: 'ec235f7d-b435-e27d-86c6-c8fef3180a01',
+ cloud_provider: 'amazon'
+ }
+ }
+ end
+ let(:scc_response_body) do
+ {
+ id: 1234567,
+ login: 'SCC_3b336b126db1503a9513a14e92a6a62e',
+ password: '24f057b7941e80f9cf2d51e16e8af2d6'
+ }.to_json
+ end
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(InstanceVerification::Exception, 'Custom plugin error')
+ 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)
.to_return(
status: 401,
- body: 'bar',
+ body: { error: 'Instance verification failed: The product is not available for this instance' }.to_json,
headers: {}
)
+ # stub the fake announcement call PAYG has to do to SCC
+ # to create the system before activate product (and skip validation)
+ stub_request(:post, 'https://scc.suse.com/connect/subscriptions/systems')
+ .to_return(status: 201, body: scc_response_body, headers: {})
post url, params: payload, headers: headers
end
- it 'renders an error with exception details' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Instance verification failed: Custom plugin error')
+ context 'when the extension is not free' do
+ let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+
+ context 'when a suitable subscription is not found' do
+ let(:product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
+ )
+ end
+ let(:product_classes) { [base_product.product_class] }
+
+ it 'de-registers system from SCC and reports an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Instance verification failed: The product is not available for this instance')
+ end
+ end
end
end
- context 'when verification provider returns true' do
- let(:payload_sap) do
+ context 'when activating extensions without errors' do
+ let(:instance_data) { 'dummy_instance_data' }
+ let(:system) do
+ FactoryBot.create(
+ :system, :payg, :with_system_information, :with_activated_product, product: base_product, instance_data: instance_data
+ )
+ 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(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+ let(:payload_no_token) do
{
- identifier: product_sap.identifier.downcase,
- version: product_sap.version,
- arch: product_sap.arch
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ instance_data: 'dummy_instance_data',
+ proxy_byos_mode: system.proxy_byos_mode,
+ token: 'super_token',
+ hwinfo:
+ {
+ hostname: 'test',
+ cpus: '1',
+ sockets: '1',
+ hypervisor: 'Xen',
+ arch: 'x86_64',
+ uuid: 'ec235f7d-b435-e27d-86c6-c8fef3180a01',
+ cloud_provider: 'amazon'
+ }
}
end
- before do
- expect(InstanceVerification::Providers::Example).to receive(:new)
- .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload_sap, instance_data).and_call_original
- allow(File).to receive(:directory?)
- allow(Dir).to receive(:mkdir)
- allow(FileUtils).to receive(:touch)
- expect(InstanceVerification).to receive(:write_cache_file).once.with('repo/cache', "127.0.0.1-#{system.login}-#{product_sap.id}")
- expect(InstanceVerification).to receive(:write_cache_file).once.with('registry/cache', "127.0.0.1-#{system.login}")
- post url, params: payload_sap, headers: headers
+ let(:scc_response_body) do
+ {
+ id: 42,
+ login: 'SCC_3b336b126db1503a9513a14e92a6a62e',
+ password: '24f057b7941e80f9cf2d51e16e8af2d6'
+ }.to_json
end
- it 'renders service JSON' do
- expect(response.body).to eq(serialized_service_sap_json)
+ context 'when a suitable subscription is found' do
+ let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+
+ let(:product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
+ )
+ end
+ let(:product_classes) { [base_product.product_class, product.product_class] }
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+ let(:scc_annouce_body) do
+ {
+ hostname: system.hostname,
+ hwinfo: JSON.parse(system.system_information),
+ byos_mode: 'hybrid',
+ login: system.login,
+ password: system.password
+ }
+ end
+ let(:scc_announce_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: 'Token token=super_token',
+ 'Content-Type' => 'application/json',
+ 'User-Agent' => 'Ruby'
+ }
+ end
+
+ before do
+ 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)
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 201,
+ body: {}.to_json,
+ headers: {}
+ )
+ # stub the fake announcement call PAYG has to do to SCC
+ # to create the system before activate product (and skip validation)
+ stub_request(:post, 'https://scc.suse.com/connect/subscriptions/systems')
+ .with({ headers: scc_announce_headers, body: scc_annouce_body.to_json })
+ .to_return(status: 201, body: scc_response_body, headers: {})
+
+ expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, product.id)
+
+ post url, params: payload_no_token, headers: headers
+ end
+
+ context 'when regcode is provided' do
+ it 'returns service JSON' do
+ expect(response.body).to eq(serialized_service_json)
+ end
+ end
+ end
+
+ context 'when the extension is free' do
+ let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+ let(:product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories, :extension, free: true, base_products: [base_product]
+ )
+ end
+ let(:product_classes) { [base_product.product_class] }
+
+ before do
+ 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)
+
+ allow(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, product.id)
+ FactoryBot.create(:subscription, product_classes: product_classes)
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 201,
+ body: {}.to_json,
+ headers: {}
+ )
+ # stub the fake announcement call PAYG has to do to SCC
+ # to create the system before activate product (and skip validation)
+ stub_request(:post, 'https://scc.suse.com/connect/subscriptions/systems')
+ .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
+
+ it 'returns service JSON' do
+ expect(response.body).to eq(serialized_service_json)
+ end
end
end
end
- end
- context 'when activating extensions' do
- let(:instance_data) { 'dummy_instance_data' }
- let(:system) do
- FactoryBot.create(
- :system, :with_system_information, :with_activated_product, product: base_product, instance_data: instance_data
- )
- 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(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
-
- before do
- FactoryBot.create(:subscription, product_classes: product_classes)
- expect(InstanceVerification::Providers::Example).not_to receive(:new)
- stub_request(:post, scc_activate_url)
- .to_return(
- status: 401,
- body: 'bar',
- headers: {}
- )
-
- post url, params: payload, headers: headers
- 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 the extension is not free' do
- let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+ context "when system doesn't have hw_info" do
+ let(:system) { FactoryBot.create(:system, :hybrid) }
- context 'when a suitable subscription is not found' do
- let(:product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
- )
+ it 'class instance verification provider' do
+ expect(InstanceVerification::Providers::Example).to receive(:new)
+ .and_call_original.at_least(:once)
+ allow(File).to receive(:directory?)
+ allow(Dir).to receive(:mkdir)
+ allow(FileUtils).to receive(:touch)
+ post url, params: payload, headers: headers
end
- let(:product_classes) { [base_product.product_class] }
+ end
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Instance verification failed: The product is not available for this instance')
+ context 'when system has hw_info' do
+ let(:instance_data) { 'dummy_instance_data' }
+ let(:system) { FactoryBot.create(:system, :hybrid, :with_system_information, instance_data: instance_data) }
+ 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
- end
- context 'when a suitable subscription is found' do
- let(:product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
- )
+ 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
- let(:product_classes) { [base_product.product_class, product.product_class] }
- it 'returns service JSON' do
- expect(response.body).to eq(serialized_service_json)
+ 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).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
+
+ it 'renders an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ end
+ end
+
+ 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).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
+
+ it 'renders an error with exception details' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ end
end
end
end
- context 'when the extension is free' do
- let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
- let(:product) do
+ context 'when activating extensions with errors' do
+ let(:instance_data) { 'dummy_instance_data' }
+ let(:system) do
FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: true, base_products: [base_product]
- )
+ :system, :hybrid, :with_system_information, :with_activated_product, product: base_product, instance_data: instance_data
+ )
+ 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(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+ let(:payload) do
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ instance_data: 'dummy_instance_data',
+ proxy_byos_mode: :hybrid,
+ hwinfo:
+ {
+ hostname: 'test',
+ cpus: '1',
+ sockets: '1',
+ hypervisor: 'Xen',
+ arch: 'x86_64',
+ uuid: 'ec235f7d-b435-e27d-86c6-c8fef3180a01',
+ cloud_provider: 'amazon'
+ }
+ }
end
- let(:product_classes) { [base_product.product_class] }
- it 'returns service JSON' do
- expect(response.body).to eq(serialized_service_json)
+ let(:scc_response_body) do
+ {
+ id: 1234567,
+ login: 'SCC_3b336b126db1503a9513a14e92a6a62e',
+ password: '24f057b7941e80f9cf2d51e16e8af2d6'
+ }.to_json
end
- end
+ before do
+ 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)
+ .to_return(
+ status: 401,
+ body: { error: 'Instance verification failed: The product is not available for this instance' }.to_json,
+ headers: {}
+ )
+ # stub the fake announcement call PAYG has to do to SCC
+ # to create the system before activate product (and skip validation)
+ stub_request(:post, 'https://scc.suse.com/connect/subscriptions/systems')
+ .to_return(status: 201, body: scc_response_body, headers: {})
- context 'when the base product subscription is missing' do
- let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
- let(:product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
- )
+ post url, params: payload, headers: headers
end
- let(:product_classes) { [] }
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ context 'when the extension is not free' do
+ let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+
+ context 'when a suitable subscription is found' do
+ let(:product) do
+ FactoryBot.create(
+ :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
+ )
+ end
+ let(:product_classes) { [base_product.product_class, product.product_class] }
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+
+ 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
+ end
end
end
end
@@ -221,66 +575,178 @@
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) }
+ 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)
+ 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 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
- 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
end
+# rubocop:enable RSpec/NestedGroups
diff --git a/engines/registration_sharing/app/controllers/registration_sharing/rmt_to_rmt_controller.rb b/engines/registration_sharing/app/controllers/registration_sharing/rmt_to_rmt_controller.rb
index 7d06ddef6..66cb118d2 100644
--- a/engines/registration_sharing/app/controllers/registration_sharing/rmt_to_rmt_controller.rb
+++ b/engines/registration_sharing/app/controllers/registration_sharing/rmt_to_rmt_controller.rb
@@ -10,6 +10,13 @@ def create
system = System.find_or_create_by(login: params[:login])
system.update(system_params)
+ # TODO: remove this block when proxy_byos column gets dropped
+ if !params.key?(:proxy_byos_mode) && system.attribute_names.include?('proxy_byos_mode')
+ # the info comes from a sibling that does not have proxy_byos_mode
+ # to a sibling does have proxy_byos_mode
+ system.proxy_byos_mode = system.proxy_byos ? :byos : :payg
+ end
+ # end todo
system.activations = []
params[:activations].each do |activation|
product = Product.find_by(id: activation[:product_id])
@@ -34,7 +41,7 @@ def destroy
protected
def system_params
- params.permit(:login, :password, :hostname, :proxy_byos, :system_token, :registered_at, :created_at, :last_seen_at, :instance_data)
+ params.permit(:login, :password, :hostname, :proxy_byos, :proxy_byos_mode, :system_token, :registered_at, :created_at, :last_seen_at, :instance_data)
end
def authenticate
diff --git a/engines/registration_sharing/lib/registration_sharing/client.rb b/engines/registration_sharing/lib/registration_sharing/client.rb
index 3028facb9..6ac9e35cd 100644
--- a/engines/registration_sharing/lib/registration_sharing/client.rb
+++ b/engines/registration_sharing/lib/registration_sharing/client.rb
@@ -21,7 +21,7 @@ def sync_system
def peer_register_system(system)
params = {}
- %w[login password hostname proxy_byos system_token registered_at created_at last_seen_at instance_data].each do |attribute|
+ %w[login password hostname proxy_byos proxy_byos_mode system_token registered_at created_at last_seen_at instance_data].each do |attribute|
params[attribute] = system.send(attribute)
end
diff --git a/engines/registration_sharing/spec/lib/registration_sharing/client_spec.rb b/engines/registration_sharing/spec/lib/registration_sharing/client_spec.rb
index 4c9ce8ce1..38c1c9e67 100644
--- a/engines/registration_sharing/spec/lib/registration_sharing/client_spec.rb
+++ b/engines/registration_sharing/spec/lib/registration_sharing/client_spec.rb
@@ -32,7 +32,8 @@
'login' => system.login,
'password' => system.password,
'hostname' => system.hostname,
- 'proxy_byos' => system.proxy_byos,
+ 'proxy_byos' => system.byos?,
+ 'proxy_byos_mode' => system.proxy_byos_mode,
'system_token' => system.system_token,
'registered_at' => system.registered_at,
'created_at' => system.created_at,
diff --git a/engines/registration_sharing/spec/requests/registration_sharing/rmt_to_rmt_controller_spec.rb b/engines/registration_sharing/spec/requests/registration_sharing/rmt_to_rmt_controller_spec.rb
index f03558390..d08c3a722 100644
--- a/engines/registration_sharing/spec/requests/registration_sharing/rmt_to_rmt_controller_spec.rb
+++ b/engines/registration_sharing/spec/requests/registration_sharing/rmt_to_rmt_controller_spec.rb
@@ -1,9 +1,11 @@
require 'rails_helper'
require 'securerandom'
+# rubocop:disable Metrics/ModuleLength
module RegistrationSharing
RSpec.describe RmtToRmtController, type: :request do
- let(:login) { SecureRandom.hex }
+ let(:login_payg) { SecureRandom.hex }
+ let(:login_byos) { SecureRandom.hex }
let(:password) { SecureRandom.hex }
let(:created_at) { Time.zone.now.round - 60 }
let(:registered_at) { created_at + 5 }
@@ -18,16 +20,17 @@ module RegistrationSharing
allow(Settings).to receive(:[]).with(:regsharing).and_return({ api_secret: api_secret })
end
- describe '#create' do
+ describe '#create byos' do
before do
post(
'/api/regsharing',
params: {
- login: login,
+ login: login_byos,
password: password,
created_at: created_at,
registered_at: registered_at,
last_seen_at: last_seen_at,
+ proxy_byos: true,
activations: [
{
product_id: product.id,
@@ -54,20 +57,21 @@ module RegistrationSharing
end
context 'system' do
- subject(:system) { System.find_by(login: login) }
+ subject(:system) { System.find_by(login: login_byos) }
it { is_expected.not_to eq(nil) }
its(:password) { is_expected.to eq(password) }
its(:created_at) { is_expected.to eq(created_at) }
its(:registered_at) { is_expected.to eq(registered_at) }
its(:last_seen_at) { is_expected.to eq(last_seen_at) }
+ its(:proxy_byos_mode) { is_expected.to eq('byos') }
it 'saves instance data' do
expect(system.instance_data).to eq(instance_data)
end
end
context 'activation' do
- subject(:activation) { System.find_by(login: login).activations.first }
+ subject(:activation) { System.find_by(login: login_byos).activations.first }
it { is_expected.not_to eq(nil) }
it 'has correct product_id' do
@@ -78,6 +82,87 @@ module RegistrationSharing
end
end
+ describe '#create payg' do
+ before do
+ post(
+ '/api/regsharing',
+ params: {
+ login: login_payg,
+ password: password,
+ created_at: created_at,
+ registered_at: registered_at,
+ last_seen_at: last_seen_at,
+ proxy_byos: false,
+ activations: [
+ {
+ product_id: product.id,
+ created_at: created_at
+ }
+ ],
+ instance_data: instance_data
+ },
+ headers: { 'Authorization' => "Bearer #{request_token}" }
+ )
+ end
+
+ context 'with correct credentials' do
+ it 'performs HTTP request successfully' do
+ expect(response).to have_http_status(204)
+ end
+
+ context 'system' do
+ subject(:system) { System.find_by(login: login_payg) }
+
+ it { is_expected.not_to eq(nil) }
+ its(:proxy_byos_mode) { is_expected.to eq('payg') }
+ it 'saves instance data' do
+ expect(system.instance_data).to eq(instance_data)
+ end
+ end
+ end
+ end
+
+ describe '#create hybrid' do
+ before do
+ post(
+ '/api/regsharing',
+ params: {
+ login: login_payg,
+ password: password,
+ created_at: created_at,
+ registered_at: registered_at,
+ last_seen_at: last_seen_at,
+ proxy_byos_mode: :hybrid,
+ activations: [
+ {
+ product_id: product.id,
+ created_at: created_at
+ }
+ ],
+ instance_data: instance_data
+ },
+ headers: { 'Authorization' => "Bearer #{request_token}" }
+ )
+ end
+
+ context 'with correct credentials' do
+ it 'performs HTTP request successfully' do
+ expect(response).to have_http_status(204)
+ end
+
+ context 'system' do
+ subject(:system) { System.find_by(login: login_payg) }
+
+ it { is_expected.not_to eq(nil) }
+ its(:proxy_byos_mode) { is_expected.to eq('hybrid') }
+ its(:proxy_byos) { is_expected.to eq(false) }
+ it 'saves instance data' do
+ expect(system.instance_data).to eq(instance_data)
+ end
+ end
+ end
+ end
+
describe '#destroy' do
let!(:system) { FactoryBot.create(:system) }
@@ -109,3 +194,4 @@ module RegistrationSharing
end
end
end
+# rubocop:enable Metrics/ModuleLength
diff --git a/engines/registry/app/controllers/registry/registry_controller.rb b/engines/registry/app/controllers/registry/registry_controller.rb
index 4dc26b84e..2eccc6f7f 100644
--- a/engines/registry/app/controllers/registry/registry_controller.rb
+++ b/engines/registry/app/controllers/registry/registry_controller.rb
@@ -1,4 +1,6 @@
module Registry
+ class RegistryAuthError < ArgumentError; end
+
class RegistryController < Registry::ApplicationController
REGISTRY_SERVICE = 'SUSE Linux OCI Registry'.freeze
REGISTRY_API_VERSION = 'registry/2.0'.freeze
@@ -19,12 +21,23 @@ def authorize
# Returns a Distribution Registry HTTP API V2 - compatible repository catalog as defined in
# https://distribution.github.io/distribution/spec/api/#listing-repositories
def catalog
+ raise RegistryAuthError, 'Could not find system with current credentials' unless @client && @client.account
+
access_scope = AccessScope.parse('registry:catalog:*')
repos = access_scope.allowed_paths(System.find_by(login: @client&.account))
logger.debug("Returning #{repos.size} repos for client #{@client}")
response.set_header('Docker-Distribution-Api-Version', REGISTRY_API_VERSION)
render json: { repositories: repos }, status: :ok
+ rescue RegistryAuthError => e
+ logger.error("Could not search in catalog: #{e.message}")
+ error = if e.message.include?('credentials')
+ ActionController::TranslatedError.new(N_('Please, re-authenticate'))
+ else
+ ActionController::TranslatedError.new(N_(e.message))
+ end
+ error.status = :unauthorized
+ render json: { error: error.message }.to_json, status: :unauthorized
end
private
diff --git a/engines/registry/app/models/access_scope.rb b/engines/registry/app/models/access_scope.rb
index dece46643..b15c0b17b 100644
--- a/engines/registry/app/models/access_scope.rb
+++ b/engines/registry/app/models/access_scope.rb
@@ -83,11 +83,30 @@ def allowed_paths(system = nil)
access_policies_yml = YAML.safe_load(
File.read(Rails.application.config.access_policies)
)
- active_products = system.activations.includes(:product).pluck(:product_class)
-
- allowed_products = (active_products & access_policies_yml.keys)
- allowed_glob_paths = access_policies_yml.values_at(*allowed_products).flatten
-
+ active_product_classes = system.activations.includes(:product).pluck(:product_class)
+ 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, product_type: 'extension').free? }.compact
+ unless allowed_non_free_product_classes.empty?
+ auth_header = {
+ '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, 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]}"
+ )
+ # remove the non active non free product extension from the allowed paths
+ allowed_product_classes -= [non_free_prod_class]
+ end
+ end
+ end
+ end
+ allowed_glob_paths = access_policies_yml.values_at(*allowed_product_classes).flatten
@allowed_paths = parse_repos(repo_list, allowed_glob_paths)
end
diff --git a/engines/registry/app/models/registry/authenticated_client.rb b/engines/registry/app/models/registry/authenticated_client.rb
index 3a63902ac..2b34baa59 100644
--- a/engines/registry/app/models/registry/authenticated_client.rb
+++ b/engines/registry/app/models/registry/authenticated_client.rb
@@ -1,28 +1,19 @@
class Registry::AuthenticatedClient
include RegistryClient
- attr_reader :auth_strategy
-
def initialize(login, password, remote_ip)
raise Registry::Exceptions::InvalidCredentials.new(message: 'expired credentials', login: login) unless cache_file_exist?(remote_ip, login)
- authenticate_by_system_credentials(login, password)
- if @auth_strategy
- Rails.logger.info("Authenticated '#{self}'")
- else
- raise Registry::Exceptions::InvalidCredentials.new(login: login)
- end
+ raise Registry::Exceptions::InvalidCredentials.new(login: login) unless authenticate_by_system_credentials(login, password)
+
+ Rails.logger.info("Authenticated '#{self}'")
end
private
def authenticate_by_system_credentials(login, password)
@systems = System.get_by_credentials(login, password)
- if @systems.present?
- @account = login
- @auth_strategy = :system_credentials
- end
- @auth_strategy
+ @account = login if @systems.present?
end
def cache_file_exist?(remote_ip, login)
diff --git a/engines/registry/lib/registry/engine.rb b/engines/registry/lib/registry/engine.rb
index aa5481f12..4aacf2f59 100644
--- a/engines/registry/lib/registry/engine.rb
+++ b/engines/registry/lib/registry/engine.rb
@@ -12,9 +12,11 @@ class Engine < ::Rails::Engine
config.after_initialize do
Api::Connect::V3::Systems::ActivationsController.class_eval do
- before_action :handle_auth_cache, only: %w[index]
+ # only run instance verification if the instance metadata is present
+ # and run the cache refresh if instance metadata gets verified
+ before_action :refresh_auth_cache, only: %w[index], if: -> { request.headers['X-Instance-Data'] }
- def handle_auth_cache
+ def refresh_auth_cache
unless ZypperAuth.verify_instance(request, logger, @system)
render(xml: { error: 'Instance verification failed' }, status: :forbidden)
end
diff --git a/engines/registry/spec/app/controllers/registry_controller_spec.rb b/engines/registry/spec/app/controllers/registry_controller_spec.rb
index c1e9cfcb1..471d9c188 100644
--- a/engines/registry/spec/app/controllers/registry_controller_spec.rb
+++ b/engines/registry/spec/app/controllers/registry_controller_spec.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Metrics/ModuleLength
module Registry
describe RegistryController, type: :request do
describe '#authenticate' do
@@ -45,8 +46,6 @@ module Registry
end
describe '#catalog access' do
- let(:system) { create(:system) }
- let(:auth_headers) { { 'Authorization' => ActionController::HttpAuthentication::Basic.encode_credentials(system.login, system.password) } }
let(:auth_headers_token) { {} }
let(:fake_response) { { repositories: repositories_returned } }
@@ -55,57 +54,138 @@ module Registry
end
let(:authorize_url) { 'api/registry/authorize' }
let(:root_url) { 'smt-ec2.susecloud.net' }
- let(:params_catalog) { "account=#{system.login}&scope=registry:catalog:*&service=SUSE%20Linux%20OCI%20Registry" }
let(:access_policy_content) { File.read('engines/registry/spec/data/access_policy_yaml.yml') }
let(:registry_conf) { { root_url: root_url } }
- before do
- stub_request(:get, "https://registry-example.susecloud.net/api/registry/authorize?account=#{system.login}&scope=registry:catalog:*&service=SUSE%20Linux%20OCI%20Registry")
- .with(
- headers: {
- 'Accept' => '*/*',
- 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
- 'User-Agent' => 'Ruby'
- }
-).to_return(status: 200, body: JSON.dump({ foo: 'foo' }), headers: {})
+ context 'with valid credentials' do
+ let(:system) { create(:system) }
+ let(:auth_headers) { { 'Authorization' => ActionController::HttpAuthentication::Basic.encode_credentials(system.login, system.password) } }
+ let(:params_catalog) { "account=#{system.login}&scope=registry:catalog:*&service=SUSE%20Linux%20OCI%20Registry" }
+
+ before do
+ stub_request(:get, "https://registry-example.susecloud.net/api/registry/authorize?account=#{system.login}&scope=registry:catalog:*&service=SUSE%20Linux%20OCI%20Registry")
+ .with(
+ headers: {
+ 'Accept' => '*/*',
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
+ 'User-Agent' => 'Ruby'
+ }
+ ).to_return(status: 200, body: JSON.dump({ foo: 'foo' }), headers: {})
+
+ stub_request(:get, "#{RegistryCatalogService.new.catalog_api_url}?n=1000")
+ .to_return(body: JSON.dump(fake_response), status: 200, headers: { 'Content-type' => 'application/json' })
+ end
- stub_request(:get, "#{RegistryCatalogService.new.catalog_api_url}?n=1000")
- .to_return(body: JSON.dump(fake_response), status: 200, headers: { 'Content-type' => 'application/json' })
- end
+ context 'with a valid token' do
+ it 'has catalog access' do
+ allow(File).to receive(:read).and_return(access_policy_content)
+ allow_any_instance_of(AuthenticatedClient).to receive(:cache_file_exist?).and_return(true)
+ get(
+ '/api/registry/authorize',
+ params: { service: 'SUSE Linux OCI Registry', scope: 'registry:catalog:*' },
+ headers: auth_headers
+ )
+
+ auth_headers_token['Authorization'] = format("Bearer #{json_response[:token]}")
+ get('/api/registry/catalog', headers: auth_headers_token)
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
- context 'with a valid token' do
- it 'has catalog access' do
- allow(File).to receive(:read).and_return(access_policy_content)
- allow_any_instance_of(AuthenticatedClient).to receive(:cache_file_exist?).and_return(true)
- get(
- '/api/registry/authorize',
- params: { service: 'SUSE Linux OCI Registry', scope: 'registry:catalog:*' },
- headers: auth_headers
- )
+ context 'when token is invalid' do
+ it 'denies the access' do
+ get(
+ '/api/registry/authorize',
+ params: { service: 'SUSE Linux OCI Registry', scope: 'registry:catalog:*' },
+ headers: auth_headers
+ )
- auth_headers_token['Authorization'] = format("Bearer #{json_response[:token]}")
- get('/api/registry/catalog', headers: auth_headers_token)
+ auth_headers_token['Authorization'] = format('Bearer foo')
- expect(response).to have_http_status(:ok)
+ get('/api/registry/catalog', headers: auth_headers_token)
+
+ expect(response).to have_http_status(:unauthorized)
+ end
end
- end
- context 'when token is invalid' do
- it 'raise an exception' do
- get(
- '/api/registry/authorize',
- params: { service: 'SUSE Linux OCI Registry', scope: 'registry:catalog:*' },
- headers: auth_headers
- )
+ context 'when an error happens' do
+ it 'denies the access' do
+ allow_any_instance_of(AccessScope).to receive(:allowed_paths).and_raise(RegistryAuthError, 'Foo')
+ allow(File).to receive(:read).and_return(access_policy_content)
+ allow_any_instance_of(AuthenticatedClient).to receive(:cache_file_exist?).and_return(true)
+ get(
+ '/api/registry/authorize',
+ params: { service: 'SUSE Linux OCI Registry', scope: 'registry:catalog:*' },
+ headers: auth_headers
+ )
+
+ auth_headers_token['Authorization'] = format("Bearer #{json_response[:token]}")
+ get('/api/registry/catalog', headers: auth_headers_token)
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
- auth_headers_token['Authorization'] = format('Bearer foo')
+ context 'with invalid credentials' do
+ let(:system) { create(:system) }
+ let(:auth_headers) { { 'Authorization' => ActionController::HttpAuthentication::Basic.encode_credentials(system.login, system.password) } }
+ let(:jwt_payload) do
+ [
+ {
+ 'iss' => 'RMT',
+ 'sub' => nil,
+ 'aud' => 'SUSE Linux OCI Registry',
+ 'exp' => 1724155172,
+ 'nbf' => 1724154872,
+ 'iat' => 1724154872,
+ 'jti' => 'NWRhY2VlYTAtNWE1Mi00NmYzLWI4MTEtZDdiYzRkYjE1OWRm',
+ 'access' => [{ 'type' => 'registry', 'class' => nil, 'name' => 'catalog', 'actions' => ['*'] }]
+ },
+ {
+ 'kid' => 'C7TL:6AHY:F4L2:PJT2:QSOT:AACT:QPDE:VPK3:3BEG:SJNF:Q52E:OIZR',
+ 'alg' => 'RS256'
+ }
+ ]
+ end
- get('/api/registry/catalog', headers: auth_headers_token)
+ before do
+ allow(JWT).to receive(:decode).and_return(jwt_payload)
+ allow_any_instance_of(Registry::AuthenticatedClient).to receive(:cache_file_exist?).and_return(true)
+ stub_request(:get, 'https://registry-example.susecloud.net/api/registry/authorize?&scope=registry:catalog:*&service=SUSE%20Linux%20OCI%20Registry')
+ .with(
+ headers: {
+ 'Accept' => '*/*',
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
+ 'User-Agent' => 'Ruby'
+ }
+ ).to_return(status: 200, body: JSON.dump({ foo: 'foo' }), headers: {})
+
+ stub_request(:get, "#{RegistryCatalogService.new.catalog_api_url}?n=1000")
+ .to_return(body: JSON.dump(fake_response), status: 200, headers: { 'Content-type' => 'application/json' })
+ end
- expect(response).to have_http_status(:unauthorized)
+ context 'with a valid token' do
+ it 'can not find system' do
+ allow(File).to receive(:read).and_return(access_policy_content)
+ allow_any_instance_of(AuthenticatedClient).to receive(:cache_file_exist?).and_return(true)
+ get(
+ '/api/registry/authorize',
+ params: { service: 'SUSE Linux OCI Registry', scope: 'registry:catalog:*' },
+ headers: auth_headers
+ )
+
+ auth_headers_token['Authorization'] = format("Bearer #{json_response[:token]}")
+ get('/api/registry/catalog', headers: auth_headers_token)
+
+ expect(response).to have_http_status(:unauthorized)
+ expect(JSON.parse(body)['error']).to eq('Please, re-authenticate')
+ end
end
end
end
end
end
+# rubocop:enable Metrics/ModuleLength
diff --git a/engines/registry/spec/app/models/access_scope_spec.rb b/engines/registry/spec/app/models/access_scope_spec.rb
index 1af1d75b1..0d3c84a22 100644
--- a/engines/registry/spec/app/models/access_scope_spec.rb
+++ b/engines/registry/spec/app/models/access_scope_spec.rb
@@ -105,6 +105,7 @@
end
end
+ # rubocop:disable RSpec/NestedGroups
describe ".granted['actions']" do
let(:product1) do
product = FactoryBot.create(:product, :with_mirrored_repositories)
@@ -152,16 +153,16 @@
type: 'a',
name: 'suse/sles/*',
actions: ['pull']
- )
+ )
end
it 'returns default auth actions (no free repos included)' do
yaml_string = access_policy_content
data = YAML.safe_load yaml_string
- data[product1.product_class] = 'suse/**'
+ data[product1.product_class] = 'suse/sles/**'
File.write('engines/registry/spec/data/access_policy_yaml.yml', YAML.dump(data))
allow_any_instance_of(RegistryCatalogService).to receive(:repos).and_return(['suse/sles/super_repo'])
- allow(File).to receive(:read).and_return(access_policy_content)
+ allow(File).to receive(:read).and_return(File.read('engines/registry/spec/data/access_policy_yaml.yml'))
possible_access = access_scope.granted(client: client)
expect(possible_access).to eq(
@@ -171,7 +172,77 @@
'class' => nil,
'name' => 'suse/sles/*'
}
+ )
+ end
+ end
+
+ context 'when product is LTSS' do
+ subject(:access_scope) do
+ described_class.new(
+ type: 'a',
+ name: 'suse/ltss/**',
+ actions: ['pull']
+ )
+ end
+
+ let(:system) do
+ system = FactoryBot.create(:system, :hybrid)
+ system.activations << [
+ FactoryBot.create(:activation, system: system, service: product1.service),
+ FactoryBot.create(:activation, system: system, service: product2.service)
+ ]
+ system
+ end
+ let(:product1) do
+ 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
+ end
+
+ context 'when activation expired' do
+ let(:scc_response) do
+ {
+ is_active: false,
+ message: 'You shall not have access to those repos !'
+ }
+ end
+ let(:header_expected) do
+ { '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,
+ 'SLES15-SP4-LTSS-X86'
+ ).and_return(scc_response)
+ end
+
+ it 'returns no actions allowed' do
+ expect(SccProxy).to receive(:scc_check_subscription_expiration).with(
+ header_expected,
+ system,
+ 'SLES15-SP4-LTSS-X86'
+ )
+ yaml_string = access_policy_content
+ data = YAML.safe_load yaml_string
+ data[product1.product_class] = 'suse/ltss/**'
+ File.write('engines/registry/spec/data/access_policy_yaml.yml', YAML.dump(data))
+ allow_any_instance_of(RegistryCatalogService).to receive(:repos).and_return(['suse/ltss/ltss_repo'])
+ allow(File).to receive(:read).and_return(File.read('engines/registry/spec/data/access_policy_yaml.yml'))
+ possible_access = access_scope.granted(client: client)
+
+ expect(possible_access).to eq(
+ {
+ 'type' => 'a',
+ 'actions' => [],
+ 'class' => nil,
+ 'name' => 'suse/ltss/**'
+ }
)
+ end
end
end
@@ -226,4 +297,5 @@
end
end
end
+ # rubocop:enable RSpec/NestedGroups
end
diff --git a/engines/registry/spec/app/models/registry/authenticated_client_spec.rb b/engines/registry/spec/app/models/registry/authenticated_client_spec.rb
index fef79b66a..2f20d5a5a 100644
--- a/engines/registry/spec/app/models/registry/authenticated_client_spec.rb
+++ b/engines/registry/spec/app/models/registry/authenticated_client_spec.rb
@@ -24,7 +24,7 @@
it 'returns the auth strategy' do
expect(client.systems).to eq([system])
- expect(client.auth_strategy).to eq(:system_credentials)
+ expect(client.account).to eq(system.login)
end
end
diff --git a/engines/registry/spec/data/access_policy_yaml.yml b/engines/registry/spec/data/access_policy_yaml.yml
index 5d6f778c3..ce006e751 100644
--- a/engines/registry/spec/data/access_policy_yaml.yml
+++ b/engines/registry/spec/data/access_policy_yaml.yml
@@ -23,4 +23,3 @@ free:
- suse/sles/**
- suse/vmdp/**
- trento/**
-72AAA: suse/**
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 fced324ea..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,20 +3,36 @@
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
- before do
- headers['X-Instance-Data'] = 'IMDS'
- allow(ZypperAuth).to receive(:verify_instance).and_return(true)
- end
-
context 'without X-Instance-Data headers or hw_info' do
- it 'has service URLs with HTTP scheme' do
+ it 'does not update InstanceVerification cache' do
get '/connect/systems/activations', headers: headers
data = JSON.parse(response.body)
expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud})
+ expect(InstanceVerification).not_to receive(:update_cache)
+ end
+ end
+
+ context 'with X-Instance-Data headers and bad metadata' do
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+
+ before do
+ headers['X-Instance-Data'] = 'IMDS'
+ Thread.current[:logger] = RMT::Logger.new('/dev/null')
+ end
+
+ it 'does not update InstanceVerification cache' do
+ allow(plugin_double).to(
+ receive(:instance_valid?)
+ .and_raise(InstanceVerification::Exception, 'Custom plugin error')
+ )
+ allow(ZypperAuth).to receive(:verify_instance).and_call_original
+ get '/connect/systems/activations', headers: headers
+ expect(response.body).to include('Instance verification failed')
+ expect(InstanceVerification).not_to receive(:update_cache)
end
end
end
@@ -26,10 +42,6 @@
before do
allow(File).to receive(:join).and_call_original
- # allow(File).to receive(:exist?).with('repo/cache/127.0.0.1-login45-1052122')
- # allow(File).to receive(:exist?).with("repo/cache/127.0.0.1-#{system.login}-#{system.products.first.id}").and_return(true)
- # allow(File).to receive(:exist?).with("repo/cache/127.0.0.1-#{system.login}-#{system.products.first.id}").and_return(true)
- # allow(File).to receive(:exist?)
allow(InstanceVerification).to receive(:update_cache)
allow(ZypperAuth).to receive(:verify_instance).and_call_original
headers['X-Instance-Data'] = 'IMDS'
@@ -45,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 ef80a9b3b..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))
@@ -80,24 +67,50 @@ def prepare_scc_announce_request(uri_path, auth, params)
# to SCC.
# SCC will make sure to handle the data correctly. This removes the need
# to adapt here if information send by the client changes.
- scc_request.body = {
+ scc_req_body = {
hostname: params['hostname'],
hwinfo: params['hwinfo'],
- byos: true
- }.to_json
+ byos_mode: params['proxy_byos_mode']
+ }
+ # when system is payg, we do not know whether it's hybrid or not
+ # we send the login and password information to skip the validation
+ # on the SCC side, that info is enough to validate the product later on
+ # if the system is, in fact, hybrid
+ # When activating a BYOS extension on top of a PAYG system ('hybrid mode'),
+ # the system already has credentials in RMT, but is not known to SCC.
+ # We announce it to SCC including its credentials in this case.
+ if params['proxy_byos_mode'] == 'hybrid'
+ scc_req_body[:login] = params['scc_login']
+ scc_req_body[:password] = params['scc_password']
+ end
+ scc_request.body = scc_req_body.to_json
scc_request
end
- def prepare_scc_request(uri_path, product, auth, token, email)
- scc_request = Net::HTTP::Post.new(uri_path, headers(auth, nil))
+ def prepare_scc_request(uri_path, product, auth, params, mode)
+ params_header = params
+ params_header = nil if mode == 'byos'
+
+ scc_request = Net::HTTP::Post.new(uri_path, headers(auth, params_header))
scc_request.body = {
- token: token,
+ token: params[:token] || nil,
identifier: product.identifier,
version: product.version,
arch: product.arch,
release_type: product.release_type,
- email: email || nil,
- byos: true
+ email: params[:email] || nil,
+ byos_mode: mode
+ }.to_json
+ 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
@@ -113,15 +126,26 @@ def announce_system_scc(auth, params)
JSON.parse(response.body)
end
- def scc_activate_product(product, auth, token, email)
- 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, token, email)
- http.request(scc_request)
+ scc_request = prepare_scc_request(uri.path, product, auth, params, mode)
+ 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
@@ -132,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)
@@ -150,48 +188,69 @@ def parse_error(error_message, token = nil, email = nil)
error_message
end
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def scc_check_subscription_expiration(headers, login, system_token, logger)
- auth = headers['HTTP_AUTHORIZATION'] if headers.include?('HTTP_AUTHORIZATION')
+ 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: true })
- scc_request = Net::HTTP::Get.new(uri.path, headers(auth, system_token))
+ 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
- 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 }
+ Rails.logger.info "Could not get the system (#{system.login}) activations, error: #{response.message} #{response.code}"
+ raise ActionController::TranslatedError.new(response.body)
end
- scc_systems_activations = JSON.parse(response.body)
- return { is_active: false, message: 'No activations.' } if scc_systems_activations.empty?
+ JSON.parse(response.body)
+ end
- no_status_products_ids = scc_systems_activations.map do |act|
- act['service']['product']['id'] if (act['status'].nil? && act['expires_at'].nil?)
- end.flatten.compact
- return { is_active: true } unless no_status_products_ids.all?(&:nil?)
+ def product_path_access(x_original_uri, products_ids)
+ products = Product.where(id: products_ids)
+ product_paths = products.map { |prod| prod.repositories.pluck(:local_path) }.flatten
+ product_paths.any? { |path| x_original_uri.include?(path) }
+ end
+
+ def product_class_access(scc_systems_activations, product)
+ active_products_names = scc_systems_activations.map { |act| act['service']['product']['product_class'] if act['status'].casecmp('active').zero? }.flatten
+ if active_products_names.include?(product)
+ { is_active: true }
+ else
+ expired_products_names = scc_systems_activations.map do |act|
+ act['service']['product']['product_class'] unless act['status'].casecmp('active').zero?
+ end.flatten
+ message = if expired_products_names.all?(&:nil?)
+ 'Product not activated.'
+ elsif expired_products_names.include?(product)
+ 'Subscription expired.'
+ else
+ 'Unexpected error when checking product subscription.'
+ end
+ { is_active: false, message: message }
+ 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
- products = Product.where(id: active_products_ids)
- product_paths = products.map { |prod| prod.repositories.pluck(:local_path) }.flatten
- active_subscription = product_paths.any? { |path| headers.fetch('X-Original-URI', '').include?(path) }
- if active_subscription
+ 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
# product not found in the active subscriptions, check the expired ones
expired_products_ids = scc_systems_activations.map { |act| act['service']['product']['id'] unless act['status'].casecmp('active').zero? }.flatten
if expired_products_ids.all?(&:nil?)
- return {
+ {
is_active: false,
message: 'Product not activated.'
}
end
- products = Product.where(id: expired_products_ids)
- product_paths = products.map { |prod| prod.repositories.pluck(:local_path) }.flatten
- expired_subscription = product_paths.any? { |path| headers.fetch('X-Original-URI', '').include?(path) }
- if expired_subscription
+ if SccProxy.product_path_access(x_original_uri, expired_products_ids)
{
is_active: false,
message: 'Subscription expired.'
@@ -205,9 +264,37 @@ def scc_check_subscription_expiration(headers, login, system_token, logger)
end
end
# rubocop:enable Metrics/PerceivedComplexity
- # rubocop:enable Metrics/CyclomaticComplexity
+
+ 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|
+ act['service']['product']['id'] if (act['status'].nil? && act['expires_at'].nil?)
+ end.flatten.compact
+ 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
+ # rubocop:disable Metrics/ClassLength
class Engine < ::Rails::Engine
isolate_namespace SccProxy
config.generators.api_only = true
@@ -219,20 +306,28 @@ 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)
+ @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)
@system = System.create!(
system_token: SccProxy.instance_id,
login: response['login'],
password: response['password'],
hostname: params[:hostname],
+ proxy_byos_mode: :byos,
proxy_byos: true,
- system_information: system_information
+ system_information: system_information,
+ instance_data: instance_data
)
end
logger.info("System '#{@system.hostname}' announced")
@@ -260,24 +355,78 @@ 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
def scc_activate_product
- logger.info "Activating product #{@product.product_string} to SCC"
- auth = request.headers['HTTP_AUTHORIZATION']
- if @system.proxy_byos
- response = SccProxy.scc_activate_product(@product, auth, params[:token], params[:email])
- 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'
- raise ActionController::TranslatedError.new(error['error'])
- 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
+ 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
+ @system.hybrid! if mode == 'hybrid' && @system.payg?
+
logger.info "Product #{@product.product_string} successfully activated with SCC"
InstanceVerification.update_cache(request.remote_ip, @system.login, @product.id)
end
end
+
+ 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
+ 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
Api::Connect::V4::Systems::ProductsController.class_eval do
@@ -287,16 +436,29 @@ def scc_activate_product
def scc_deactivate_product
auth = request.headers['HTTP_AUTHORIZATION']
- if @system.proxy_byos && @product[:product_type] != 'base'
- response = SccProxy.deactivate_product_scc(auth, @product, @system.system_token)
- 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'])
+ if @system.byos? && @product[:product_type] != 'base'
+ 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_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
+ 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
- logger.info "Product '#{@product.friendly_name}' successfully deactivated from SCC"
end
+ logger.info "Product '#{@product.friendly_name}' successfully deactivated from SCC"
+ 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
@@ -306,16 +468,9 @@ def scc_deactivate_product
protected
def scc_deregistration
- if @system.proxy_byos
- 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'
+ if @system.byos? || @system.hybrid?
+ # byos and hybrid systems should get de-register from SCC
+ SccProxy.deregister_system_scc(request.headers['HTTP_AUTHORIZATION'], @system)
end
end
end
@@ -352,7 +507,7 @@ def authenticate_system(skip_on_duplicated: false)
def get_system(systems)
return nil if systems.blank?
- byos_systems_with_token = systems.select { |system| system.proxy_byos && system.system_token }
+ byos_systems_with_token = systems.select { |system| system.byos? && system.system_token }
return systems.first if byos_systems_with_token.empty?
@@ -379,5 +534,6 @@ def get_system(systems)
end
end
end
+ # rubocop:enable Metrics/ClassLength
end
# rubocop:enable Metrics/ModuleLength
diff --git a/engines/scc_proxy/spec/requests/api/connect/v3/subscriptions/systems_controller_spec.rb b/engines/scc_proxy/spec/requests/api/connect/v3/subscriptions/systems_controller_spec.rb
index 150cb6864..442bfd9cf 100644
--- a/engines/scc_proxy/spec/requests/api/connect/v3/subscriptions/systems_controller_spec.rb
+++ b/engines/scc_proxy/spec/requests/api/connect/v3/subscriptions/systems_controller_spec.rb
@@ -17,6 +17,90 @@
let(:params) do
{
hostname: 'test',
+ proxy_byos_mode: :byos,
+ instance_data: instance_data,
+ hwinfo:
+ {
+ hostname: 'test',
+ cpus: '1',
+ sockets: '1',
+ hypervisor: 'Xen',
+ arch: 'x86_64',
+ uuid: 'ec235f7d-b435-e27d-86c6-c8fef3180a01',
+ cloud_provider: 'super_cloud'
+ }
+ }
+ end
+
+ context 'valid credentials' do
+ before do
+ stub_request(:post, scc_register_system_url)
+ .to_return(
+ status: 201,
+ body: scc_register_response.to_s,
+ headers: {}
+ )
+ end
+
+ it 'saves the data' do
+ post '/connect/subscriptions/systems', params: params, headers: { HTTP_AUTHORIZATION: 'Token token=bar' }
+ system = System.find_by(login: 'SCC_foo')
+ expect(system.instance_data).to eq(instance_data)
+ end
+ end
+
+ context 'credentials not found' do
+ before do
+ stub_request(:post, scc_register_system_url)
+ .to_return(
+ status: [401, 'Unauthorized'],
+ body: '{}',
+ headers: {}
+ )
+ end
+
+ it 'returns error' do
+ post '/connect/subscriptions/systems', params: params, headers: { HTTP_AUTHORIZATION: 'Token token=bar' }
+ data = JSON.parse(response.body)
+ expect(response.code).to eq('401')
+ expect(data['type']).to eq('error')
+ expect(data['error']).to include('Unauthorized')
+ end
+ end
+
+ context 'unreachable server' do
+ before do
+ stub_request(:post, scc_register_system_url)
+ .to_return(
+ status: 408,
+ body: scc_register_response.to_s,
+ headers: {}
+ )
+ end
+
+ it 'returns error' do
+ post '/connect/subscriptions/systems', params: params, headers: { HTTP_AUTHORIZATION: 'Token token=bar' }
+ data = JSON.parse(response.body)
+ expect(data['type']).to eq('error')
+ expect(data['error']).to eq('408 ""')
+ end
+ end
+ end
+
+ context 'using SCC generated credentials (PAYG/LTSS mode)' do
+ let(:scc_register_system_url) { 'https://scc.suse.com/connect/subscriptions/systems' }
+ let(:scc_register_response) do
+ {
+ id: 5684096,
+ login: 'SCC_foo',
+ password: '1234',
+ last_seen_at: '2021-10-24T09:48:52.658Z'
+ }.to_json
+ end
+ let(:params) do
+ {
+ hostname: 'test',
+ proxy_byos_mode: :payg,
instance_data: instance_data,
hwinfo:
{
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 b0ef27a1f..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
@@ -3,307 +3,613 @@
# rubocop:disable RSpec/NestedGroups
describe Api::Connect::V3::Systems::ProductsController, type: :request do
- include_context 'auth header', :system, :login, :password
- include_context 'version header', 3
-
let(:url) { connect_systems_products_url }
- let(:headers) { auth_header.merge(version_header) }
- let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions) }
let(:product_sap) { FactoryBot.create(:product, :product_sles_sap, :with_mirrored_repositories, :with_mirrored_extensions) }
-
- let(:payload) do
- {
- identifier: product.identifier,
- version: product.version,
- arch: product.arch
- }
- end
- let(:payload_byos) do
- {
- identifier: product.identifier,
- version: product.version,
- arch: product.arch,
- email: 'foo',
- token: 'bar'
- }
- end
+ let(:instance_data) { '' }
+ let(:scc_register_system_url) { 'https://scc.suse.com/connect/subscriptions/systems' }
+ let(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
+ let(:scc_systems_url) { 'https://scc.suse.com/connect/systems' }
describe '#activate' do
let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
- context 'when system has hw_info' do
- let(:instance_data) { '{"instanceId": "dummy_instance_data"}' }
- let(:new_system_token) { 'BBBBBBBB-BBBB-4BBB-9BBB-BBBBBBBBBBBB' }
- let(:system) { FactoryBot.create(:system, :with_system_information, instance_data: instance_data) }
- 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) do
- FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data,
- system_token: new_system_token)
- end
- let(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
- let(:subscription_response) do
+ context 'when system is BYOS' do
+ include_context 'auth header', :system_byos, :login, :password
+ include_context 'version header', 3
+ let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions) }
+ let(:headers) { auth_header.merge(version_header) }
+
+ let(:payload_byos) do
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ email: 'foo',
+ token: 'bar',
+ byos_mode: 'byos',
+ hwinfo:
{
- 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'
- }
- ]
+ hostname: 'test',
+ cpus: '1',
+ sockets: '1',
+ hypervisor: 'Xen',
+ arch: 'x86_64',
+ uuid: 'ec235f7d-b435-e27d-86c6-c8fef3180a01',
+ cloud_provider: 'amazon'
}
- 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"}',
- 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('repo/cache', "127.0.0.1-#{system.login}-#{product.id}")
- allow(InstanceVerification).to receive(:write_cache_file).twice.with('registry/cache', "127.0.0.1-#{system.login}")
- end
+ }
+ end
- it 'renders service JSON' do
- post url, params: payload_byos, headers: headers
- expect(response.body).to eq(serialized_service_json)
- 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(:system_byos) { FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data) }
+ 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
- context 'with a not valid registration code' do
- before do
- stub_request(:post, scc_activate_url)
- .to_return(
- status: 401,
- body: '{"error": "No product found on SCC for: foo bar x86_64 json api"}',
- headers: {}
- )
- allow(InstanceVerification).to receive(:write_cache_file).twice.with('repo/cache', "127.0.0.1-#{system.login}-#{product.id}")
- allow(InstanceVerification).to receive(:write_cache_file).twice.with('registry/cache', "127.0.0.1-#{system.login}")
- allow(FileUtils).to receive(:mkdir_p)
- allow(FileUtils).to receive(:touch)
-
- post url, params: payload_byos, headers: headers
- end
-
- it 'renders an error with exception details' do
- data = JSON.parse(response.body)
- expect(data['error']).to include('No product found on SCC')
- expect(data['error']).not_to include('json api')
- 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 'with different system_tokens' do
- let(:system2) do
+ context 'when system is connected to SCC' do
+ let(:system_byos) do
FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data,
- system_token: 'foo')
+ system_token: new_system_token)
+ 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(System).to receive(:get_by_credentials).and_return([system, system2])
allow(plugin_double).to(
receive(:instance_valid?)
.and_raise(InstanceVerification::Exception, 'Custom plugin error')
)
- stub_request(:post, scc_activate_url)
- .to_return(
- status: 201,
- body: '{"id": "bar"}',
- headers: {}
+ 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_byos.login}-#{product.id}"
+ )
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}"
)
- allow(InstanceVerification).to receive(:write_cache_file).twice.with('repo/cache', "127.0.0.1-#{system.login}-#{product.id}")
- allow(InstanceVerification).to receive(:write_cache_file).twice.with('registry/cache', "127.0.0.1-#{system.login}")
- allow(File).to receive(:directory?)
- allow(FileUtils).to receive(:mkdir_p)
- allow(FileUtils).to receive(:touch)
+ end
- post url, params: payload_byos, headers: headers
+ it 'renders service JSON' do
+ post url, params: payload_byos, headers: headers
+ expect(response.body).to eq(serialized_service_json)
+ end
end
- it 'renders service JSON' do
- expect(response.body).to eq(serialized_service_json)
- end
- end
+ context 'with a not valid registration code' do
+ before do
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 401,
+ body: { error: 'No product found on SCC for: foo bar x86_64 json api' }.to_json,
+ headers: {}
+ )
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_byos.login}-#{product.id}"
+ )
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}"
+ )
+ allow(FileUtils).to receive(:mkdir_p)
+ allow(FileUtils).to receive(:touch)
- context 'with duplicated system_tokens' do
- let(:system3) do
- FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data,
- system_token: 'foo')
+ post url, params: payload_byos, headers: headers
+ end
+
+ it 'renders an error with exception details' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to include('No product found on SCC')
+ expect(data['error']).not_to include('json api')
+ end
end
- before do
- system3 = system
- system3.save!
- allow(System).to receive(:get_by_credentials).and_return([system, system3])
- allow(plugin_double).to(
- receive(:instance_valid?)
- .and_raise(InstanceVerification::Exception, 'Custom plugin error')
- )
- headers['System-Token'] = 'foo'
- stub_request(:post, scc_activate_url)
- .to_return(
- status: 201,
- body: '{"id": "bar"}',
- headers: {}
+ context 'with different system_tokens' do
+ let(:system_byos2) do
+ FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data,
+ system_token: 'foo')
+ end
+
+ before do
+ allow(System).to receive(:get_by_credentials).and_return([system_byos, system_byos2])
+ allow(plugin_double).to(
+ receive(:instance_valid?)
+ .and_raise(InstanceVerification::Exception, 'Custom plugin error')
+ )
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 201,
+ body: { id: 'bar' }.to_json,
+ headers: {}
+ )
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_byos.login}-#{product.id}"
)
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}"
+ )
+ 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('repo/cache', "127.0.0.1-#{system.login}-#{product.id}")
- allow(InstanceVerification).to receive(:write_cache_file).twice.with('registry/cache', "127.0.0.1-#{system.login}")
- allow(File).to receive(:directory?)
- allow(FileUtils).to receive(:mkdir_p)
- allow(FileUtils).to receive(:touch)
+ post url, params: payload_byos, headers: headers
+ end
- post url, params: payload_byos, headers: headers
+ it 'renders service JSON' do
+ expect(response.body).to eq(serialized_service_json)
+ end
end
- it 'renders service JSON' do
- expect(response.body).to eq(serialized_service_json)
+ context 'with duplicated system_tokens' do
+ let(:system_byos3) do
+ FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data,
+ system_token: 'foo')
+ end
+
+ before do
+ system_byos3 = system_byos
+ system_byos3.save!
+ allow(System).to receive(:get_by_credentials).and_return([system_byos, system_byos3])
+ allow(plugin_double).to(
+ receive(:instance_valid?)
+ .and_raise(InstanceVerification::Exception, 'Custom plugin error')
+ )
+ headers['System-Token'] = 'foo'
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 201,
+ body: { id: 'bar' }.to_json,
+ headers: {}
+ )
+
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_byos.login}-#{product.id}"
+ )
+ allow(InstanceVerification).to receive(:write_cache_file).twice.with(
+ Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}"
+ )
+ allow(File).to receive(:directory?)
+ allow(FileUtils).to receive(:mkdir_p)
+ allow(FileUtils).to receive(:touch)
+
+ post url, params: payload_byos, headers: headers
+ end
+
+ it 'renders service JSON' do
+ expect(response.body).to eq(serialized_service_json)
+ end
end
end
end
end
- end
- context 'when activating extensions' do
- let(:instance_data) { 'dummy_instance_data' }
- let(:system) do
- FactoryBot.create(
- :system, :with_system_information, :with_activated_product, product: base_product, instance_data: instance_data
- )
- 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(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' }
-
- before do
- FactoryBot.create(:subscription, product_classes: product_classes)
- stub_request(:post, scc_activate_url)
- .to_return(
- status: 401,
- body: 'bar',
- headers: {}
+ context 'when activating extensions for BYOS' do
+ let(:instance_data) { 'dummy_instance_data' }
+ let(:system_byos) do
+ FactoryBot.create(
+ :system, :byos, :with_system_information, :with_activated_product, product: base_product, instance_data: instance_data
)
+ 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(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+ let(:payload_byos) do
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ email: 'foo',
+ token: 'bar',
+ byos_mode: 'byos',
+ hwinfo:
+ {
+ hostname: 'test',
+ cpus: '1',
+ sockets: '1',
+ hypervisor: 'Xen',
+ arch: 'x86_64',
+ uuid: 'ec235f7d-b435-e27d-86c6-c8fef3180a01',
+ cloud_provider: 'amazon'
+ }
+ }
+ end
- post url, params: payload, headers: headers
- end
+ before do
+ allow(InstanceVerification::Providers::Example).to receive(:new)
+ .with(nil, nil, nil, 'dummy_instance_data').and_return(plugin_double)
+ allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' })
+ FactoryBot.create(:subscription, product_classes: product_classes)
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 401,
+ body: { error: 'Instance verification failed: The product is not available for this instance' }.to_json,
+ headers: {}
+ )
+ # stub the fake announcement call PAYG has to do to SCC
+ # to create the system before activate product (and skip validation)
+ stub_request(:post, scc_register_system_url)
+ .to_return(status: 201, body: { ok: 'OK' }.to_json, headers: {})
- context 'when the extension is not free' do
- let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
+ post url, params: payload_byos, headers: headers
+ end
+ end
- context 'when a suitable subscription is not found' do
- let(:product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
+ context 'when system is PAYG' do
+ include_context 'auth header', :system_payg, :login, :password
+ include_context 'version header', 3
+ let(:headers) { auth_header.merge(version_header) }
+ let(:system_payg) { FactoryBot.create(:system, :payg, :with_system_information, :with_activated_base_product, instance_data: instance_data) }
+ let(:product) do
+ FactoryBot.create(
+ :product, :product_sles, :extension, :with_mirrored_repositories, :with_mirrored_extensions,
+ base_products: [system_payg.products.first]
)
- end
- let(:product_classes) { [base_product.product_class] }
+ end
+ let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') }
+ let(:payload) do
+ {
+ identifier: product.identifier,
+ version: product.version,
+ arch: product.arch,
+ instance_data: instance_data,
+ token: 'bar',
+ byos_mode: 'hybrid',
+ hwinfo:
+ {
+ hostname: 'super_test',
+ cpus: '1',
+ sockets: '1',
+ hypervisor: 'Xen',
+ arch: 'x86_64',
+ uuid: 'ec235f7d-b435-e27d-86c6-c8fef3180a01',
+ cloud_provider: 'amazon'
+ }
+ }
+ end
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Instance verification failed: The product is not available for this instance')
- expect(InstanceVerification::Providers::Example).not_to receive(:new)
+ 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
- end
- context 'when a suitable subscription is found' do
- let(:product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
- )
+ 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
- let(:product_classes) { [base_product.product_class, product.product_class] }
- it 'returns service JSON' do
- expect(response.body).to eq(serialized_service_json)
+ 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::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
- end
- context 'when the extension is free' do
- let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
- let(:product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: true, base_products: [base_product]
- )
- end
- let(:product_classes) { [base_product.product_class] }
+ 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
- it 'returns service JSON' do
- expect(response.body).to eq(serialized_service_json)
- end
- 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
- context 'when the base product subscription is missing' do
- let(:base_product) { FactoryBot.create(:product, :with_mirrored_repositories) }
- let(:product) do
- FactoryBot.create(
- :product, :with_mirrored_repositories, :extension, free: false, base_products: [base_product]
- )
- end
- let(:product_classes) { [] }
+ before do
+ allow(plugin_double).to(
+ receive(:instance_valid?)
+ .and_raise(InstanceVerification::Exception, 'Custom plugin error')
+ )
+ end
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq('Unexpected instance verification error has occurred')
+ 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}"
+ )
+ 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
+
+ it 'renders service JSON' do
+ stub_request(:post, scc_register_system_url)
+ .to_return(status: 201, body: { ok: 'OK' }.to_json, headers: {})
+
+ post url, params: payload, headers: headers
+ expect(response.body).to eq(serialized_service_json)
+ end
+ end
+
+ context 'with a not valid registration code' do
+ before do
+ stub_request(:post, scc_activate_url)
+ .to_return(
+ status: 401,
+ body: { error: 'No product found on SCC for: foo bar x86_64 json api' }.to_json,
+ headers: {}
+ )
+ 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(FileUtils).to receive(:mkdir_p)
+ allow(FileUtils).to receive(:touch)
+ end
+
+ context 'when de-register system from SCC suceeds' do
+ before do
+ stub_request(:delete, scc_systems_url)
+ .to_return(
+ status: 204,
+ body: '',
+ headers: {}
+ )
+
+ 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
+ data = JSON.parse(response.body)
+ expect(data['error']).to include('No product found on SCC')
+ expect(data['error']).not_to include('json api')
+ end
+ 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,
+ body: { error: 'Could not de-register system' }.to_json,
+ headers: {}
+ )
+ stub_request(:post, scc_register_system_url)
+ .to_return(status: 201, body: { ok: 'OK' }.to_json, 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
+ end
+ end
+ end
end
end
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 73cafc798..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
@@ -1,21 +1,97 @@
describe Api::Connect::V4::Systems::ProductsController, type: :request do
- include_context 'auth header', :system, :login, :password
- include_context 'version header', 4
-
-
let(:url) { connect_systems_products_url }
let(:headers) { auth_header.merge(version_header) }
let(:instance_data) { 'dummy_instance_data' }
- let(:system) { FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data) }
describe '#deactivate' do
- let(:scc_systems_products_url) { 'https://scc.suse.com/connect/systems/products' }
+ context 'when system is byos' do
+ include_context 'auth header', :system_byos, :login, :password
+ include_context 'version header', 4
+ let(:scc_systems_products_url) { 'https://scc.suse.com/connect/systems/products' }
+ let(:system_byos) { FactoryBot.create(:system, :byos, :with_system_information, instance_data: instance_data) }
+ let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
+
+ # rubocop:disable RSpec/NestedGroups
+ context 'an activated non base module' do
+ context 'with right credentials' do
+ let(:product) do
+ FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_byos,
+product_type: 'module')
+ 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
- context 'an activated non base module' do
- context 'with right credentials' do
- let(:product) do
- FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system, product_type: 'module')
+ before do
+ stub_request(:delete, scc_systems_products_url)
+ .to_return(
+ status: 200,
+ body: '',
+ headers: {}
+ )
+ allow(Rails.logger).to receive(:info)
+ delete url, params: payload, headers: headers
+ end
+
+ it 'returns a service JSON and successfully deactivate the product' do
+ expect(Rails.logger).to(
+ have_received(:info).with(
+ "Product '#{product.friendly_name}' successfully deactivated from SCC"
+ ).once
+ )
+ expect(response.body).to eq(serialized_service_json)
+ end
end
+
+ context 'when SCC API returns an error' do
+ let(:product) do
+ FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_byos,
+product_type: 'module')
+ 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
+
+ before do
+ stub_request(:delete, scc_systems_products_url)
+ .to_return(
+ status: 422,
+ body: "{\"error\": \"Could not de-activate product \'#{product.friendly_name}\'\"}",
+ 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
+ end
+ # rubocop:enable RSpec/NestedGroups
+
+ context 'an activated base module with right credentials' do
+ let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_byos) }
let(:payload) do
{
identifier: product.identifier,
@@ -23,38 +99,234 @@
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
+
+ before { delete url, params: payload, headers: headers }
+
+ it 'reports an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq("The product \"#{product.name}\" is a base product and cannot be deactivated")
end
+ end
+ end
- before do
- stub_request(:delete, scc_systems_products_url)
- .to_return(
- status: 200,
- body: '',
- headers: {}
- )
- allow(Rails.logger).to receive(:info)
- delete url, params: payload, headers: headers
+ # rubocop:disable RSpec/NestedGroups
+ context 'when system is hybrid' do
+ include_context 'auth header', :system_hybrid, :login, :password
+ include_context 'version header', 4
+ let(:scc_systems_products_url) { 'https://scc.suse.com/connect/systems/products' }
+ let(:system_hybrid) { FactoryBot.create(:system, :hybrid, :with_system_information, instance_data: instance_data) }
+
+ context 'an activated non base module' do
+ context 'with right credentials' do
+ let(:product) do
+ FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_hybrid,
+product_type: 'module')
+ 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
+
+ before do
+ stub_request(:delete, scc_systems_products_url)
+ .to_return(
+ status: 200,
+ body: '',
+ headers: {}
+ )
+ allow(Rails.logger).to receive(:info)
+ delete url, params: payload, headers: headers
+ end
+
+ it 'returns a service JSON and successfully deactivate the product' do
+ expect(Rails.logger).to(
+ have_received(:info).with(
+ "Product '#{product.friendly_name}' successfully deactivated from SCC"
+ ).once
+ )
+ expect(response.body).to eq(serialized_service_json)
+ end
+ end
+
+ context 'when SCC API for activations returns an error' 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(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
+
+ before do
+ stub_request(:get, scc_systems_activations_url).to_return(status: 401, body: "{\"error\": \"Error\'\"}", headers: {})
+ delete url, params: payload, headers: headers
+ end
+
+ it 'reports an error' do
+ data = JSON.parse(response.body)
+ expect(data['error']).to eq("{\"error\": \"Error'\"}")
+ end
end
- it 'returns a service JSON and successfully deactivate the product' do
- expect(Rails.logger).to(
- have_received(:info).with(
- "Product '#{product.friendly_name}' successfully deactivated from SCC"
- ).once
- )
- expect(response.body).to eq(serialized_service_json)
+ 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
+ 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(: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: 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, 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
+ }
+ }
+ }].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,
+ 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'
+ }
+ }
+ }
+ ].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
- context 'when SCC API returns an error' do
- let(:product) do
- FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system, product_type: 'module')
- end
+ context 'an activated base module with right credentials' do
+ let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system_hybrid) }
let(:payload) do
{
identifier: product.identifier,
@@ -62,46 +334,15 @@
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
- before do
- stub_request(:delete, scc_systems_products_url)
- .to_return(
- status: 422,
- body: "{\"error\": \"Could not de-activate product \'#{product.friendly_name}\'\"}",
- headers: {}
- )
- delete url, params: payload, headers: headers
- end
+ before { delete url, params: payload, headers: headers }
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\'')
+ expect(data['error']).to eq("The product \"#{product.name}\" is a base product and cannot be deactivated")
end
end
end
-
- context 'an activated base module with right credentials' do
- let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions, :activated, system: system) }
- let(:payload) do
- {
- identifier: product.identifier,
- version: product.version,
- arch: product.arch
- }
- end
-
- before { delete url, params: payload, headers: headers }
-
- it 'reports an error' do
- data = JSON.parse(response.body)
- expect(data['error']).to eq("The product \"#{product.name}\" is a base product and cannot be deactivated")
- end
- end
+ # rubocop:enable RSpec/NestedGroups
end
end
diff --git a/engines/scc_suma_api/app/controllers/scc_suma_api/scc_suma_api_controller.rb b/engines/scc_suma_api/app/controllers/scc_suma_api/scc_suma_api_controller.rb
index a7e67cc30..879fc7c06 100644
--- a/engines/scc_suma_api/app/controllers/scc_suma_api/scc_suma_api_controller.rb
+++ b/engines/scc_suma_api/app/controllers/scc_suma_api/scc_suma_api_controller.rb
@@ -48,7 +48,9 @@ def is_valid?
)
# check auth for registered BYOS systems
iid = verification_provider.parse_instance_data
- systems_found = System.find_by(system_token: iid['instanceId'], proxy_byos: true)
+ # at this point, we do not know nor is available the login information of the system
+ # so querying the instance ID, which is a unique value, to fetch the system
+ systems_found = System.find_by(system_token: iid['instanceId'], proxy_byos_mode: :byos)
raise 'Unspecified error' unless systems_found.present? || verification_provider.instance_valid?
end
diff --git a/engines/scc_suma_api/spec/requests/scc_suma_api/scc_suma_api_controller_spec.rb b/engines/scc_suma_api/spec/requests/scc_suma_api/scc_suma_api_controller_spec.rb
index 0013320c6..bf3a9c4b1 100644
--- a/engines/scc_suma_api/spec/requests/scc_suma_api/scc_suma_api_controller_spec.rb
+++ b/engines/scc_suma_api/spec/requests/scc_suma_api/scc_suma_api_controller_spec.rb
@@ -67,7 +67,7 @@ module SccSumaApi
allow_any_instance_of(InstanceVerification::Providers::Example).to(
receive(:instance_valid?).and_return(false)
)
- System.stub(:find_by).and_return('foo')
+ allow(System).to receive(:find_by).and_return 'foo'
allow(SUSE::Connect::Api).to receive(:new).and_return api_double
allow(api_double).to receive(:list_products_unscoped).and_return products
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 1c87a5de6..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,31 +31,15 @@ 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, 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
- message = ''
- if system.proxy_byos
- result = SccProxy.scc_check_subscription_expiration(request.headers, system.login, system.system_token, logger)
- if result[:is_active]
- InstanceVerification.update_cache(request.remote_ip, system.login, base_product.id)
- return true
- end
-
- message = result[:message]
- else
- message = e.message
- end
- details = [ "System login: #{system.login}", "IP: #{request.remote_ip}" ]
- details << "Instance ID: #{verification_provider.instance_id}" if verification_provider.instance_id
- details << "Billing info: #{verification_provider.instance_billing_info}" if verification_provider.instance_billing_info
-
- ZypperAuth.auth_logger.info <<~LOGMSG
- Access to the repos denied: #{message}
- #{details.join(', ')}
- LOGMSG
+ return handle_scc_subscription(request, system, verification_provider) if system.byos?
+ ZypperAuth.zypper_auth_message(request, system, verification_provider, e.message)
false
rescue StandardError => e
logger.error('Unexpected instance verification error has occurred:')
@@ -65,6 +49,26 @@ def verify_instance(request, logger, system)
logger.error(e.backtrace)
false
end
+
+ 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])
+ false
+ end
+
+ def zypper_auth_message(request, system, verification_provider, message)
+ details = [ "System login: #{system.login}", "IP: #{request.remote_ip}" ]
+ details << "Instance ID: #{verification_provider.instance_id}" if verification_provider.instance_id
+ details << "Billing info: #{verification_provider.instance_billing_info}" if verification_provider.instance_billing_info
+
+ ZypperAuth.auth_logger.info <<~LOGMSG
+ Access to the repos denied: #{message}
+ #{details.join(', ')}
+ LOGMSG
+ end
end
class Engine < ::Rails::Engine
@@ -126,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/api/connect/v3/systems/activations_controller_spec.rb b/engines/zypper_auth/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
index 892ac28df..10500b262 100644
--- a/engines/zypper_auth/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
+++ b/engines/zypper_auth/spec/requests/api/connect/v3/systems/activations_controller_spec.rb
@@ -16,7 +16,10 @@
context 'without X-Instance-Data headers or hw_info' do
it 'has service URLs with HTTP scheme' do
- expect(response.body).to include('Instance verification failed')
+ data = JSON.parse(response.body)
+ expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud})
+ expect_any_instance_of(InstanceVerification::Providers::Example).not_to receive(:instance_valid?)
+ expect(InstanceVerification).not_to receive(:update_cache)
end
end
@@ -24,7 +27,10 @@
let(:system) { FactoryBot.create(:system, :with_activated_product, :with_system_information, instance_data: 'plugin:susecloud') }
it 'has service URLs with HTTP scheme' do
- expect(response.body).to include('Instance verification failed')
+ data = JSON.parse(response.body)
+ expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud})
+ expect_any_instance_of(InstanceVerification::Providers::Example).not_to receive(:instance_valid?)
+ expect(InstanceVerification).not_to receive(:update_cache)
end
end
diff --git a/engines/zypper_auth/spec/requests/api/connect/v3/systems/products_controller_spec.rb b/engines/zypper_auth/spec/requests/api/connect/v3/systems/products_controller_spec.rb
index ee610d3f8..0d21e2e51 100644
--- a/engines/zypper_auth/spec/requests/api/connect/v3/systems/products_controller_spec.rb
+++ b/engines/zypper_auth/spec/requests/api/connect/v3/systems/products_controller_spec.rb
@@ -14,7 +14,8 @@
{
identifier: product.identifier.downcase,
version: product.version,
- arch: product.arch
+ arch: product.arch,
+ byos_mode: 'byos'
}
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 bc7d1d8bf..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
@@ -6,7 +6,11 @@
let(:system) { FactoryBot.create(:system, :with_activated_product) }
+ after { FileUtils.rm_rf(File.dirname(Rails.application.config.registry_cache_dir)) }
+
describe '#check' do
+ before { Thread.current[:logger] = RMT::Logger.new('/dev/null') }
+
context 'with valid credentials' do
include_context 'auth header', :system, :login, :password
@@ -130,7 +134,7 @@
context 'when subscription is active' do
before do
stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_active].to_json, headers: {})
- expect(URI).to receive(:encode_www_form).with({ byos: true })
+ expect(URI).to receive(:encode_www_form).with({ byos_mode: 'byos' })
allow(File).to receive(:directory?).and_return(true)
allow(Dir).to receive(:mkdir)
allow(FileUtils).to receive(:touch)
@@ -143,7 +147,7 @@
context 'when subscription is expired' do
before do
stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_expired].to_json, headers: {})
- expect(URI).to receive(:encode_www_form).with({ byos: true })
+ expect(URI).to receive(:encode_www_form).with({ byos_mode: 'byos' })
get '/api/auth/check', headers: headers
end
@@ -153,7 +157,7 @@
context 'when product is not activated' do
before do
stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_not_activated].to_json, headers: {})
- expect(URI).to receive(:encode_www_form).with({ byos: true })
+ expect(URI).to receive(:encode_www_form).with({ byos_mode: 'byos' })
get '/api/auth/check', headers: headers
end
@@ -163,7 +167,7 @@
context 'when status from SCC is unknown' do
before do
stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_unknown_status].to_json, headers: {})
- expect(URI).to receive(:encode_www_form).with({ byos: true })
+ expect(URI).to receive(:encode_www_form).with({ byos_mode: 'byos' })
allow(File).to receive(:directory?)
allow(Dir).to receive(:mkdir)
allow(FileUtils).to receive(:touch)
@@ -176,7 +180,7 @@
context 'when SCC request fails' do
before do
stub_request(:get, scc_systems_activations_url).to_return(status: 401, body: [body_expired].to_json, headers: {})
- expect(URI).to receive(:encode_www_form).with({ byos: true })
+ expect(URI).to receive(:encode_www_form).with({ byos_mode: 'byos' })
allow(File).to receive(:directory?)
allow(Dir).to receive(:mkdir)
allow(FileUtils).to receive(:touch)
@@ -202,6 +206,225 @@
it { is_expected.to have_http_status(200) }
end
+
+ context 'system is hybrid' do
+ include_context 'auth header', :system_hybrid, :login, :password
+ let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' }
+ let(:system_hybrid) { FactoryBot.create(:system, :hybrid, :with_activated_product) }
+ let(:requested_uri) { '/repo' + system_hybrid.repositories.first[:local_path] + '/repodata/repomd.xml' }
+ let(:headers) { auth_header.merge({ 'X-Original-URI': requested_uri, 'X-Instance-Data': 'test' }) }
+
+ before do
+ Rails.cache.clear
+ allow(InstanceVerification).to receive(:update_cache)
+ allow(Dir).to receive(:mkdir)
+ allow(FileUtils).to receive(:touch)
+ end
+
+ context 'when subscription is 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 + '-LTSS'
+ }
+ }
+ }
+ end
+ let(:headers) { auth_header }
+
+ before do
+ stub_request(:get, scc_systems_activations_url).to_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)
+ end
+
+ it 'returns true' do
+ result = SccProxy.scc_check_subscription_expiration(
+ headers,
+ system_hybrid,
+ system_hybrid.activations.first.product.product_class + '-LTSS'
+ )
+ expect(result[:is_active]).to be(true)
+ end
+ end
+
+ context 'when subscription is expired' do
+ 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 + '-LTSS'
+ }
+ }
+ }
+ end
+
+ before do
+ stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_expired].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)
+ end
+
+ it 'returns false, expired' do
+ result = SccProxy.scc_check_subscription_expiration(
+ headers,
+ system_hybrid,
+ system_hybrid.activations.first.product.product_class + '-LTSS'
+ )
+ expect(result[:is_active]).to eq(false)
+ expect(result[:message]).to eq('Subscription expired.')
+ end
+ end
+
+ context 'when product is not activated' do
+ let(:body_not_activated) 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: nil
+ }
+ }
+ }
+ end
+
+ before do
+ stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_not_activated].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)
+ end
+
+ it 'returns product not activated' do
+ result = SccProxy.scc_check_subscription_expiration(
+ headers,
+ system_hybrid,
+ system_hybrid.activations.first.product.product_class + '-LTSS'
+ )
+ expect(result[:is_active]).to eq(false)
+ expect(result[:message]).to eq('Product not activated.')
+ end
+ end
+
+ context 'when unexpected error' do
+ let(:body_unexpected) 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
+ }
+ }
+ }
+ end
+
+ before do
+ stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_unexpected].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)
+ end
+
+ it 'returns unexpected error' do
+ result = SccProxy.scc_check_subscription_expiration(
+ headers,
+ system_hybrid,
+ system_hybrid.activations.first.product.product_class + '-LTSS'
+ )
+ expect(result[:is_active]).to eq(false)
+ expect(result[:message]).to eq('Unexpected error when checking product subscription.')
+ end
+ end
+
+ context 'regcode check fails' do
+ let(:error_message) do
+ "Access to the repos denied: #{scc_response[:message]}\nSystem login: #{system_hybrid.login}, IP: 127.0.0.1\n"
+ end
+
+ let(:scc_response) do
+ {
+ is_active: false,
+ message: 'You shall not have access to those repos !'
+ }
+ end
+
+ before do
+ expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(true)
+ allow(SccProxy).to receive(:scc_check_subscription_expiration).and_return(scc_response)
+ expect(SccProxy).to receive(:scc_check_subscription_expiration)
+ allow(ZypperAuth.auth_logger).to receive(:info)
+ expect(ZypperAuth.auth_logger).to receive(:info).with(error_message)
+ expect(FileUtils).not_to receive(:touch)
+ get '/api/auth/check', headers: headers
+ end
+
+ it { is_expected.to have_http_status(403) }
+ end
+
+ context 'regcode check suceeds' do
+ let(:scc_response) do
+ {
+ is_active: true
+ }
+ end
+
+ before do
+ expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(true)
+ allow(SccProxy).to receive(:scc_check_subscription_expiration).and_return(scc_response)
+ expect(SccProxy).to receive(:scc_check_subscription_expiration)
+ allow(ZypperAuth.auth_logger).to receive(:info)
+ expect(ZypperAuth.auth_logger).not_to(receive(:info))
+ get '/api/auth/check', headers: headers
+ end
+
+ it { is_expected.to have_http_status(200) }
+ end
+ end
end
end
end
diff --git a/lib/rmt.rb b/lib/rmt.rb
index 0a90ca963..08a1fa97a 100644
--- a/lib/rmt.rb
+++ b/lib/rmt.rb
@@ -1,5 +1,5 @@
module RMT
- VERSION ||= '2.17'.freeze
+ VERSION ||= '2.19'.freeze
DEFAULT_USER = '_rmt'.freeze
DEFAULT_GROUP = 'nginx'.freeze
diff --git a/lib/rmt/cli/systems.rb b/lib/rmt/cli/systems.rb
index a7655d85c..bc35542a4 100644
--- a/lib/rmt/cli/systems.rb
+++ b/lib/rmt/cli/systems.rb
@@ -8,12 +8,12 @@ class RMT::CLI::Systems < RMT::CLI::Base
desc 'list', _('List registered systems.')
option :limit, aliases: '-l', type: :numeric, default: 20, desc: _('Number of systems to display')
option :all, aliases: '-a', type: :boolean, desc: _('List all registered systems')
- option :proxy_byos, type: :boolean, desc: _('Filter BYOS systems using RMT as a proxy')
+ option :proxy_byos_mode, type: :boolean, desc: _('Filter BYOS systems using RMT as a proxy')
option :csv, type: :boolean, desc: _('Output data in CSV format')
def list
systems = System.order(id: :desc)
- systems = systems.where(proxy_byos: true) if options.proxy_byos
+ systems = systems.where(proxy_byos_mode: :byos) if options.proxy_byos_mode
systems = systems.limit(options.limit) unless options.all
if System.count == 0
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/lib/rmt/gpg.rb b/lib/rmt/gpg.rb
index c645348f4..783b04ed1 100644
--- a/lib/rmt/gpg.rb
+++ b/lib/rmt/gpg.rb
@@ -1,6 +1,7 @@
require 'English'
require 'fileutils'
require 'tmpdir'
+require 'shellwords'
class RMT::GPG
class RMT::GPG::Exception < RuntimeError
@@ -28,7 +29,8 @@ def verify_signature
protected
def run_import_key
- cmd = "gpg --homedir #{@tmpdir} --no-default-keyring --keyring #{@keyring} --import #{@key_file} 2>&1"
+ cmd = "gpg --homedir #{@tmpdir.shellescape} --no-default-keyring " \
+ "--keyring #{@keyring.shellescape} --import #{@key_file.shellescape} 2>&1"
out = `#{cmd}`
if $CHILD_STATUS.exitstatus != 0
@@ -39,7 +41,8 @@ def run_import_key
end
def run_verify_signature
- cmd = "gpg --homedir #{@tmpdir} --no-default-keyring --keyring #{@keyring} --verify #{@signature_file} #{@metadata_file} 2>&1"
+ cmd = "gpg --homedir #{@tmpdir.shellescape} --no-default-keyring " \
+ "--keyring #{@keyring.shellescape} --verify #{@signature_file.shellescape} #{@metadata_file.shellescape} 2>&1"
out = `#{cmd}`
if $CHILD_STATUS.exitstatus != 0
diff --git a/lib/rmt/scc.rb b/lib/rmt/scc.rb
index 1ba9158ff..59fccdf2a 100644
--- a/lib/rmt/scc.rb
+++ b/lib/rmt/scc.rb
@@ -84,7 +84,7 @@ def sync_systems
scc_api_client = SUSE::Connect::Api.new(Settings.scc.username, Settings.scc.password)
# do not sync BYOS proxy systems to SCC
- systems = System.where('scc_registered_at IS NULL OR last_seen_at > scc_registered_at').where(proxy_byos: false)
+ systems = System.where('scc_registered_at IS NULL OR last_seen_at > scc_registered_at').not_byos
@logger.info(_('Syncing %{count} updated system(s) to SCC') % { count: systems.size })
begin
diff --git a/package/obs/rmt-server.changes b/package/obs/rmt-server.changes
index dffd1fbc6..16e598617 100644
--- a/package/obs/rmt-server.changes
+++ b/package/obs/rmt-server.changes
@@ -1,7 +1,39 @@
-------------------------------------------------------------------
-Fri Jun 28 23:55:02 CEST 2024 - zpetrova@suse.de
+Wed Oct 30 09:01:32 UTC 2024 - Natnael Getahun
-- fixes for RES7-LTSS and OL7-LTSS clients
+- 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)
+
+-------------------------------------------------------------------
+Wed Aug 21 15:28:43 UTC 2024 - Jesús Bermúdez Velázquez
+
+- Version 2.19
+ * Fix for mirroring products that contain special characters (eg.: '$') in their path
+ * Fix for packages with path longer 255 characters (bsc#1229152)
+ * rmt-server-pubcloud:
+ * Support registration of extensions in BYOS mode on top of a PAYG system (hybrid mode) (jsc#PCT-400)
+ * Validate repository and registy access for hybrid systems
+
+-------------------------------------------------------------------
+Mon Jul 8 09:59:59 UTC 2024 - Thomas Schmidt
+
+- Include new script to fix yum-utils issue (jsc#SLL-369)
+
+-------------------------------------------------------------------
+Mon July 1 12:42:34 UTC 2024 - Adnilson Delgado
+
+- Version 2.18
+ * Move temporary storage of downloaded files to the repo directory to avoid filling up /tmp partition. (gh:#1137)
+ * Fixes for RES7-LTSS and OL7-LTSS clients
+ * Instance Verification: re-setting the repository and registry cache path to the right value; update the cache scrubber paths
-------------------------------------------------------------------
Thu April 18 09:27:00 UTC 2024 - Adnilson Delgado
diff --git a/package/obs/rmt-server.spec b/package/obs/rmt-server.spec
index 67387a9f6..5e357fbbf 100644
--- a/package/obs/rmt-server.spec
+++ b/package/obs/rmt-server.spec
@@ -34,7 +34,7 @@
%undefine _find_debuginfo_dwz_opts
Name: rmt-server
-Version: 2.17
+Version: 2.19
Release: 0
Summary: Repository mirroring tool and registration proxy for SCC
License: GPL-2.0-or-later
@@ -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/public/tools/rmt-client-setup-res b/public/tools/rmt-client-setup-res
index 78f20dc32..e467d7d02 100755
--- a/public/tools/rmt-client-setup-res
+++ b/public/tools/rmt-client-setup-res
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
#
# rmt-client-setup-res: client use rmt-client-setup script to register with rmt.
# This script assumes SUSEConnect is already installed on the system.
@@ -119,7 +119,7 @@ else
CERTURL="$REGCERT"
fi
-$CURL --tlsv1.2 --silent --insecure --connect-timeout 10 --output $TEMPFILE $CERTURL
+$CURL --tlsv1.2 --silent --insecure --connect-timeout 10 --output "$TEMPFILE" "$CERTURL"
if [ $? -ne 0 ]; then
echo "Download failed. Abort."
exit 1
@@ -163,14 +163,12 @@ echo "Detected ${SLL_name} version: ${SLL_version}"
echo "Importing repomd.xml.key"
if [[ ${SLL_version} -eq 7 ]]; then
- $CURL --silent --show-error --insecure ${REGURL}/repo/SUSE/Updates/${SLL_name%%-LTSS}/${SLL_version}-LTSS/x86_64/update/repodata/repomd.xml.key --output repomd.xml.key
+ $CURL --silent --show-error --insecure "${REGURL}/repo/SUSE/Updates/${SLL_name%%-LTSS}/${SLL_version}-LTSS/x86_64/update/repodata/repomd.xml.key" --output repomd.xml.key
else
- $CURL --silent --show-error --insecure ${REGURL}/repo/SUSE/Updates/${SLL_name}/${SLL_version}/x86_64/update/repodata/repomd.xml.key --output repomd.xml.key
+ $CURL --silent --show-error --insecure "${REGURL}/repo/SUSE/Updates/${SLL_name}/${SLL_version}/x86_64/update/repodata/repomd.xml.key" --output repomd.xml.key
fi
$RPM --import repomd.xml.key
-if [ ! -x $SUSECONNECT ]; then
- echo "Downloading SUSEConnect"
if [[ ${SLL_version} -gt 7 ]]; then
if [ ! -x $DNF ]; then
@@ -180,14 +178,13 @@ if [[ ${SLL_version} -gt 7 ]]; then
echo "Disabling all repositories"
$DNF config-manager --disable $(dnf repolist -q | awk '{ print $1 }' | grep -v repo)
- # sed -i 's/^enabled=1/enabled=0/' /etc/yum.repos.d/*
# on RHEL9 (not RHEL8) redhat-release is protected and cannot be updated to sll-release
if [ -f /etc/dnf/protected.d/redhat-release.conf ]; then
rm -f /etc/dnf/protected.d/redhat-release.conf
fi
- $DNF config-manager --add-repo ${REGURL}/repo/SUSE/Updates/${SLL_name}/${SLL_version}/x86_64/update
- $DNF config-manager --add-repo ${REGURL}/repo/SUSE/Updates/${SLL_name}-AS/${SLL_version}/x86_64/update
+ $DNF config-manager --add-repo "${REGURL}/repo/SUSE/Updates/${SLL_name}/${SLL_version}/x86_64/update"
+ $DNF config-manager --add-repo "${REGURL}/repo/SUSE/Updates/${SLL_name}-AS/${SLL_version}/x86_64/update"
$DNF install -y --allowerasing ${SLL_release_package}
# For RHEL8/CentOS8, remove all old signing keys and import SUSE keys installed with sles_es-release package
@@ -195,17 +192,29 @@ if [[ ${SLL_version} -gt 7 ]]; then
import_rpm_signing_keys
fi
+ echo "Downloading SUSEConnect"
$DNF install SUSEConnect librepo
+
$DNF config-manager --set-disabled "${RMTNAME}_repo_SUSE_Updates_${SLL_name}_${SLL_version}_x86_64_update"
$DNF config-manager --set-disabled "${RMTNAME}_repo_SUSE_Updates_${SLL_name}-AS_${SLL_version}_x86_64_update"
elif [[ ${SLL_version} -eq 7 ]]; then
- # For SLL7 we need to have yum, yum_config_mgr, sles_os-release-server, etc..
- if [ ! -x "$YUM_CONFIG_MGR" ]; then
- echo "YUM config manager is not installed. Please install yum-config-manager and retry. Abort."
- exit 1
- fi
-
+
+ # disbale unavailable centos mirrors
+ sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo
+ sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo
+ sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo
+
+ # Check for and install yum-config-manager if not available
+ if [ ! -x $YUM_CONFIG_MGR ]; then
+ echo "yum-config-manager not found. Attempting to install."
+ $YUM install -y yum-utils
+ if [ $? -ne 0 ]; then
+ echo "Failed to install yum-config-manager. Abort."
+ exit 1
+ fi
+ fi
+
echo "Disabling all repositories"
$YUM_CONFIG_MGR --disable \* > /dev/null
@@ -215,25 +224,31 @@ elif [[ ${SLL_version} -eq 7 ]]; then
rm -f /usr/share/redhat-release
fi
- $YUM_CONFIG_MGR --add-repo ${REGURL}/repo/SUSE/Updates/${SLL_name%%-LTSS}/${SLL_version}-LTSS/x86_64/update
+ $YUM_CONFIG_MGR --add-repo "${REGURL}/repo/SUSE/Updates/${SLL_name%%-LTSS}/${SLL_version}-LTSS/x86_64/update"
if [ ${SLL_name} = "RES-OL-LTSS" ]; then
- $YUM_CONFIG_MGR --add-repo ${REGURL}/repo/SUSE/Updates/RES-BASE/${SLL_version}/x86_64/update
+ $YUM_CONFIG_MGR --add-repo "${REGURL}/repo/SUSE/Updates/RES-BASE/${SLL_version}/x86_64/update"
fi
$YUM_CONFIG_MGR --enable *suse.* > /dev/null
- $YUM install -y ${SLL_release_package} suseconnect-ng librepo
+ if [ ! -x $SUSECONNECT ]; then
+ $YUM install -y --nogpgcheck ${SLL_release_package}
+ import_rpm_signing_keys
+ $YUM install -y suseconnect-ng librepo
+ else
+ $YUM update -y ${SLL_release_package} suseconnect-ng librepo
+ fi
+
$YUM update -y yum
$YUM_CONFIG_MGR --disable \* > /dev/null
-fi
elif [[ ${SLL_version} -eq 8 ]]; then
# For SLL8, the release package is already installed, just import the keys
import_rpm_signing_keys
fi
-$CURL --silent --show-error --insecure $REGURL/tools/rmt-client-setup --output rmt-client-setup
+$CURL --silent --show-error --insecure "$REGURL/tools/rmt-client-setup" --output rmt-client-setup
echo "Running rmt-client-setup $PARAMS"
if [ -n "$YES_PARAM" ]; then
- PARAMS=$(echo $PARAMS | sed 's/--yes//')
+ PARAMS=$(echo "$PARAMS" | sed 's/--yes//')
yes | sh rmt-client-setup $PARAMS
else
sh rmt-client-setup $PARAMS
diff --git a/spec/factories/products.rb b/spec/factories/products.rb
index a660506bd..4612dfb00 100644
--- a/spec/factories/products.rb
+++ b/spec/factories/products.rb
@@ -9,7 +9,7 @@
product_type { :base }
sequence(:description) { FFaker::Lorem.sentence }
release_type { nil }
- version { 42 }
+ version { 15.3 }
arch { 'x86_64' }
release_stage { 'released' }
@@ -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 d826bb0f3..ceab7d02e 100644
--- a/spec/factories/systems.rb
+++ b/spec/factories/systems.rb
@@ -10,6 +10,14 @@
virtual { false }
end
+ trait :payg do
+ proxy_byos_mode { :payg }
+ end
+
+ trait :hybrid do
+ proxy_byos_mode { :hybrid }
+ end
+
trait :synced do
sequence(:scc_system_id) { |n| n }
@@ -19,7 +27,7 @@
end
trait :byos do
- proxy_byos { true }
+ proxy_byos_mode { :byos }
end
trait :with_activated_base_product do
@@ -49,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/gpg_spec.rb b/spec/lib/rmt/gpg_spec.rb
index 00b687432..fcc90ca2e 100644
--- a/spec/lib/rmt/gpg_spec.rb
+++ b/spec/lib/rmt/gpg_spec.rb
@@ -23,6 +23,14 @@
end
end
+ context 'when the path contains characters that need to get escaped' do
+ it 'returns true' do
+ allow(Dir).to receive(:mktmpdir).with('rmt-mirror-gpg')
+ .and_return(Dir.mktmpdir('rmt-mirr$r-gpg'))
+ expect(verifier.verify_signature).to eq(true)
+ end
+ end
+
context 'when the GPG key is invalid' do
let(:key_file) { File.join(file_fixture('gpg'), 'bad.xml.key') }
diff --git a/spec/lib/rmt/scc_spec.rb b/spec/lib/rmt/scc_spec.rb
index 94b2a739d..d1e0aa73e 100644
--- a/spec/lib/rmt/scc_spec.rb
+++ b/spec/lib/rmt/scc_spec.rb
@@ -469,6 +469,7 @@ def scc
end
let(:system) { FactoryBot.create(:system) }
+ let(:system_hybrid) { FactoryBot.create(:system, :hybrid) }
it 'syncs systems' do
expect(api_double).to receive(:send_bulk_system_update).with([system])
@@ -481,6 +482,18 @@ def scc
})
described_class.new.sync_systems
end
+
+ it 'syncs systems hybrid' do
+ expect(api_double).to receive(:send_bulk_system_update).with([system_hybrid])
+ .and_return({
+ systems: [{
+ id: 10,
+ login: system_hybrid.login,
+ password: system_hybrid.password
+ }]
+ })
+ described_class.new.sync_systems
+ 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..457a77d65 100644
--- a/spec/requests/api/connect/v3/systems/products_controller_spec.rb
+++ b/spec/requests/api/connect/v3/systems/products_controller_spec.rb
@@ -246,6 +246,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 }
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..95eede620 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,24 @@
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
end
describe '#deregister' do
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)