lib/suse/connect/api.rb
require 'optparse'
require 'cgi'
module SUSE
module Connect
# @note please take a look at https://github.com/SUSE/connect/wiki/SCC-API-(Implemented) for more detailed protocol
# description.
#
# SCC's API provides a RESTful API interface. This essentially means that you can send an HTTP request
# (GET, PUT/PATCH, POST, or DELETE) to an endpoint, and you'll get back a JSON representation of the resource(s)
# (including children) in return. The Connect API is located at https://scc.suse.com/connect.
class Api
# Set desired API version and forward it in accept headers (see connection.rb#json_request)
VERSION = 'v4'
PRODUCT_NOT_INSTALLED_INDEX = 9000
# Returns a new instance of SUSE::Connect::Api
#
# @param config [SUSE::Connect::Config] config instance
# @return [SUSE::Connect::Api] api object to call SCC API
def initialize(config)
@config = config
@connection = Connection.new(
config.url,
language: config.language,
insecure: config.insecure,
verify_callback: config.verify_callback,
debug: config.debug
)
end
# Checks if API endpoint is up-to-date, useful when dealing with RegistrationProxy errors
#
# @returns: `true` if the up-to-date SCC API detected, `false` otherwise
def up_to_date?
@connection.get('/connect/repositories/installer')
# Should fail in any case. 422 error means that the endpoint is there and working right;
# for older Registration Proxies 404 is typically expected.
# In the unlikely case this call succeeds - the API is not implemented right, so endpoint is not up-to-date.
return false
rescue ApiError => e
return e.code == 422
rescue JSON::ParserError
# Even older Registration Proxies can return html instead of json when 404 is encountered
return false
end
# Announce a system to SCC.
# @note https://github.com/SUSE/connect/wiki/SCC-API-(Implemented)#wiki-announce-system
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Token authentication where token is a registration code e.g. 'Token token=<REGCODE>'
# @return [OpenStruct] responding to #body(response from SCC), #code(natural HTTP response code) and #success.
#
def announce_system(auth, distro_target = nil, instance_data = nil, namespace = nil)
payload = {
hostname: System.hostname,
hwinfo: System.hwinfo,
distro_target: distro_target || Zypper.distro_target
}
payload[:instance_data] = instance_data if instance_data
payload[:namespace] = namespace if namespace
@connection.post('/connect/subscriptions/systems', auth: auth, params: payload)
end
# Re-send the system's hardware info to SCC.
# @note https://github.com/SUSE/connect/wiki/SCC-API-%28Implemented%29#update-system
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
# @return [OpenStruct] responding to #body(response from SCC), #code(natural HTTP response code) and #success.
#
def update_system(auth, distro_target = nil, instance_data = nil, namespace = nil)
payload = {
hostname: System.hostname,
hwinfo: System.hwinfo,
distro_target: distro_target || Zypper.distro_target
}
payload[:instance_data] = instance_data if instance_data
payload[:namespace] = namespace if namespace
@connection.put('/connect/systems', auth: auth, params: payload)
end
# Activate a product, consuming an entitlement, and receive the service for this
# combination of subscription, installed product, and architecture.
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
# @param product [SUSE::Connect::Remote::Product] product to be activated
# @param email [String] Adds the user to the respective organization or
# sends an SCC invitation.
#
# @return [OpenStruct] responding to body(response from SCC) and code(natural HTTP response code).
def activate_product(auth, product, email = nil)
payload = {
identifier: product.identifier,
version: product.version,
arch: product.arch,
release_type: product.release_type,
token: @config.token,
email: email
}
@connection.post('/connect/systems/products', auth: auth, params: payload)
end
# Deactivate a product, freeing a slot for another activation. Returns the service
# associated to the product.
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
# @param product [SUSE::Connect::Remote::Product] product to be deactivated
#
# @return [OpenStruct] responding to body(response from SCC) and code(natural HTTP response code).
def deactivate_product(auth, product)
payload = {
identifier: product.identifier,
version: product.version,
arch: product.arch,
release_type: product.release_type
}
@connection.delete('/connect/systems/products', auth: auth, params: payload)
end
# Upgrade a product and receive the updated service for the system.
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
# @param product [SUSE::Connect::Remote::Product] product
def upgrade_product(auth, product)
@connection.put('/connect/systems/products', auth: auth, params: product.to_params)
end
# Downgrade a product and receive the updated service for the system.
# INFO: Upgrade and Downgrade methods point to the same API endpoint
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
# @param product [SUSE::Connect::Remote::Product] product
alias_method :downgrade_product, :upgrade_product
# Synchronize activated system products with the registration server (SCC).
# Expects product list parameter to be a list of hashes.
#
# @param products [Array] product with identifier, arch and version defined
#
def synchronize(auth, products)
@connection.post('/connect/systems/products/synchronize', auth: auth, params: { products: products.map(&:to_params) })
end
# Show details of an (activated) product including repositories and available extensions
#
# @return [OpenStruct] responding to body(response from SCC) and code(natural HTTP response code).
#
def show_product(auth, product)
@connection.get('/connect/systems/products', auth: auth, params: product.to_params)
end
# Deregister/unregister a system
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
#
# @return [OpenStruct] responding to body(response from SCC) and code(natural HTTP response code).
#
def deregister(auth)
@connection.delete('/connect/systems', auth: auth)
end
# Gets a list of services known by the system with system credentials
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
#
# @return [OpenStruct] responding to body(response from SCC) and code(natural HTTP response code).
#
def system_services(auth)
@connection.get('/connect/systems/services', auth: auth)
end
# Gets a list of subscriptions known by system authenticated with system credentials
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
#
# @return [OpenStruct] responding to body(response from SCC) and code(natural HTTP response code).
#
def system_subscriptions(auth)
@connection.get('/connect/systems/subscriptions', auth: auth)
end
# Gets a list of activations known by system authenticated with system credentials
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
#
# @return [OpenStruct] responding to body(response from SCC) and code(natural HTTP response code).
#
def system_activations(auth)
@connection.get('/connect/systems/activations', auth: auth)
end
# Lists all available upgrade paths for a given list of products
#
# @param auth [String] authorization string which will be injected in 'Authorization' header in request.
# In this case we expect Base64 encoded string with login and password
# @param [Array <Remote::Product>] a list of producs
# @param target_base_product [Remote::Product] (optional) a target base
# product to upgrade to. Only used by the backend when kind is :offline. Defaults to nil.
# @param kind [Symbol] (optional) :online or :offline. It specifies whether
# the online or the offline migrations are desired. Defaults to :online.
#
# @return [Array <Array <Hash>>] the list of possible upgrade paths for the given products,
# where each product is represented by a hash with identifier, version, arch and release_type
def system_migrations(auth, products, target_base_product: nil, kind:)
payload = { installed_products: products.map(&:to_params) }
payload[:target_base_product] = target_base_product.to_params if target_base_product
endpoints = {
online: '/connect/systems/products/migrations',
offline: '/connect/systems/products/offline_migrations'
}
result = @connection.post(endpoints.fetch(kind), auth: auth, params: payload)
# sort migration targets by product and version
if result.body.is_a?(Array)
products_list = products.sort_by! { |product| product.isbase ? 0 : 1 }.map(&:identifier).uniq
result.body.sort_by! do |suggested_products|
[
products_list.index(suggested_products[0]['identifier']) || PRODUCT_NOT_INSTALLED_INDEX,
-suggested_products[0]['version'].to_f
]
end
end
result
end
# List available Installer-Updates repositories for the given product
#
# @param product [Remote::Product] list repositories for this product
#
# @return [Array <Hash>] list of Installer-Updates repositories
def list_installer_updates(product)
@connection.get('/connect/repositories/installer', params: product.to_params)
end
# Search packages which are available in the product of the base product
#
# @param product [SUSE::Connect::Zypper::Product] the product for in which product tree should be searched
# @param query [String] The package query to search
#
# @return [Array< <Hash>>] of all matched packages available
def package_search(product, query)
api = '/api/package_search/packages'
triplet = CGI.escape(product.to_triplet)
query = CGI.escape(query)
@connection.get(api + "?product_id=#{triplet}&query=#{query}")
rescue ApiError => e
raise e if e.code != 404
raise UnsupportedOperation, 'Package search is not supported by the '\
'registration proxy: Alternatively, use '\
'the web version at https://scc.suse.com/packages/'
end
end
end
end