diff --git a/awx/api/views/analytics.py b/awx/api/views/analytics.py index 2a4c734084..bf5ac33433 100644 --- a/awx/api/views/analytics.py +++ b/awx/api/views/analytics.py @@ -10,7 +10,7 @@ from awx.api.generics import APIView, Response from awx.api.permissions import AnalyticsPermission from awx.api.versioning import reverse from awx.main.utils import get_awx_version -from awx.main.utils.analytics_proxy import OIDCClient, DEFAULT_OIDC_TOKEN_ENDPOINT +from awx.main.utils.analytics_proxy import OIDCClient from rest_framework import status from collections import OrderedDict @@ -205,7 +205,7 @@ class AnalyticsGenericView(APIView): try: rh_user = self._get_setting('REDHAT_USERNAME', None, ERROR_MISSING_USER) rh_password = self._get_setting('REDHAT_PASSWORD', None, ERROR_MISSING_PASSWORD) - client = OIDCClient(rh_user, rh_password, DEFAULT_OIDC_TOKEN_ENDPOINT, ['api.console']) + client = OIDCClient(rh_user, rh_password) response = client.make_request( method, url, @@ -219,8 +219,8 @@ class AnalyticsGenericView(APIView): logger.error("Automation Analytics API request failed, trying base auth method") response = self._base_auth_request(request, method, url, rh_user, rh_password, headers) except MissingSettings: - rh_user = self._get_setting('SUBSCRIPTIONS_USERNAME', None, ERROR_MISSING_USER) - rh_password = self._get_setting('SUBSCRIPTIONS_PASSWORD', None, ERROR_MISSING_PASSWORD) + rh_user = self._get_setting('SUBSCRIPTIONS_CLIENT_ID', None, ERROR_MISSING_USER) + rh_password = self._get_setting('SUBSCRIPTIONS_CLIENT_SECRET', None, ERROR_MISSING_PASSWORD) response = self._base_auth_request(request, method, url, rh_user, rh_password, headers) # # Missing or wrong user/pass diff --git a/awx/api/views/root.py b/awx/api/views/root.py index af0a9c5b7b..e7bccc6080 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -32,6 +32,7 @@ from awx.api.versioning import URLPathVersioning, reverse, drf_reverse from awx.main.constants import PRIVILEGE_ESCALATION_METHODS from awx.main.models import Project, Organization, Instance, InstanceGroup, JobTemplate from awx.main.utils import set_environ +from awx.main.utils.analytics_proxy import TokenError from awx.main.utils.licensing import get_licenser logger = logging.getLogger('awx.api.views.root') @@ -176,19 +177,21 @@ class ApiV2SubscriptionView(APIView): def post(self, request): data = request.data.copy() - if data.get('subscriptions_password') == '$encrypted$': - data['subscriptions_password'] = settings.SUBSCRIPTIONS_PASSWORD + if data.get('subscriptions_client_secret') == '$encrypted$': + data['subscriptions_client_secret'] = settings.SUBSCRIPTIONS_CLIENT_SECRET try: - user, pw = data.get('subscriptions_username'), data.get('subscriptions_password') + user, pw = data.get('subscriptions_client_id'), data.get('subscriptions_client_secret') with set_environ(**settings.AWX_TASK_ENV): validated = get_licenser().validate_rh(user, pw) if user: - settings.SUBSCRIPTIONS_USERNAME = data['subscriptions_username'] + settings.SUBSCRIPTIONS_CLIENT_ID = data['subscriptions_client_id'] if pw: - settings.SUBSCRIPTIONS_PASSWORD = data['subscriptions_password'] + settings.SUBSCRIPTIONS_CLIENT_SECRET = data['subscriptions_client_secret'] except Exception as exc: msg = _("Invalid Subscription") - if isinstance(exc, requests.exceptions.HTTPError) and getattr(getattr(exc, 'response', None), 'status_code', None) == 401: + if isinstance(exc, TokenError) or ( + isinstance(exc, requests.exceptions.HTTPError) and getattr(getattr(exc, 'response', None), 'status_code', None) == 401 + ): msg = _("The provided credentials are invalid (HTTP 401).") elif isinstance(exc, requests.exceptions.ProxyError): msg = _("Unable to connect to proxy server.") @@ -215,12 +218,12 @@ class ApiV2AttachView(APIView): def post(self, request): data = request.data.copy() - pool_id = data.get('pool_id', None) - if not pool_id: - return Response({"error": _("No subscription pool ID provided.")}, status=status.HTTP_400_BAD_REQUEST) - user = getattr(settings, 'SUBSCRIPTIONS_USERNAME', None) - pw = getattr(settings, 'SUBSCRIPTIONS_PASSWORD', None) - if pool_id and user and pw: + subscription_id = data.get('subscription_id', None) + if not subscription_id: + return Response({"error": _("No subscription ID provided.")}, status=status.HTTP_400_BAD_REQUEST) + user = getattr(settings, 'SUBSCRIPTIONS_CLIENT_ID', None) + pw = getattr(settings, 'SUBSCRIPTIONS_CLIENT_SECRET', None) + if subscription_id and user and pw: data = request.data.copy() try: with set_environ(**settings.AWX_TASK_ENV): @@ -239,7 +242,7 @@ class ApiV2AttachView(APIView): logger.exception(smart_str(u"Invalid subscription submitted."), extra=dict(actor=request.user.username)) return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST) for sub in validated: - if sub['pool_id'] == pool_id: + if sub['subscription_id'] == subscription_id: sub['valid_key'] = True settings.LICENSE = sub return Response(sub) diff --git a/awx/conf/migrations/_subscriptions.py b/awx/conf/migrations/_subscriptions.py index de8320011e..a3ef48ccd1 100644 --- a/awx/conf/migrations/_subscriptions.py +++ b/awx/conf/migrations/_subscriptions.py @@ -27,5 +27,5 @@ def _migrate_setting(apps, old_key, new_key, encrypted=False): def prefill_rh_credentials(apps, schema_editor): - _migrate_setting(apps, 'REDHAT_USERNAME', 'SUBSCRIPTIONS_USERNAME', encrypted=False) - _migrate_setting(apps, 'REDHAT_PASSWORD', 'SUBSCRIPTIONS_PASSWORD', encrypted=True) + _migrate_setting(apps, 'REDHAT_USERNAME', 'SUBSCRIPTIONS_CLIENT_ID', encrypted=False) + _migrate_setting(apps, 'REDHAT_PASSWORD', 'SUBSCRIPTIONS_CLIENT_SECRET', encrypted=True) diff --git a/awx/main/analytics/core.py b/awx/main/analytics/core.py index 30eec4e503..d73d255851 100644 --- a/awx/main/analytics/core.py +++ b/awx/main/analytics/core.py @@ -22,7 +22,7 @@ from ansible_base.lib.utils.db import advisory_lock from awx.main.models import Job from awx.main.access import access_registry from awx.main.utils import get_awx_http_client_headers, set_environ, datetime_hook -from awx.main.utils.analytics_proxy import OIDCClient, DEFAULT_OIDC_TOKEN_ENDPOINT +from awx.main.utils.analytics_proxy import OIDCClient __all__ = ['register', 'gather', 'ship'] @@ -186,7 +186,7 @@ def gather(dest=None, module=None, subset=None, since=None, until=None, collecti if not ( settings.AUTOMATION_ANALYTICS_URL - and ((settings.REDHAT_USERNAME and settings.REDHAT_PASSWORD) or (settings.SUBSCRIPTIONS_USERNAME and settings.SUBSCRIPTIONS_PASSWORD)) + and ((settings.REDHAT_USERNAME and settings.REDHAT_PASSWORD) or (settings.SUBSCRIPTIONS_CLIENT_ID and settings.SUBSCRIPTIONS_CLIENT_SECRET)) ): logger.log(log_level, "Not gathering analytics, configuration is invalid. Use --dry-run to gather locally without sending.") return None @@ -368,8 +368,20 @@ def ship(path): logger.error('AUTOMATION_ANALYTICS_URL is not set') return False - rh_user = getattr(settings, 'REDHAT_USERNAME', None) - rh_password = getattr(settings, 'REDHAT_PASSWORD', None) + rh_id = getattr(settings, 'REDHAT_USERNAME', None) + rh_secret = getattr(settings, 'REDHAT_PASSWORD', None) + + if not (rh_id and rh_secret): + rh_id = getattr(settings, 'SUBSCRIPTIONS_CLIENT_ID', None) + rh_secret = getattr(settings, 'SUBSCRIPTIONS_CLIENT_SECRET', None) + + if not rh_id: + logger.error('Neither REDHAT_USERNAME nor SUBSCRIPTIONS_CLIENT_ID are set') + return False + + if not rh_secret: + logger.error('Neither REDHAT_PASSWORD nor SUBSCRIPTIONS_CLIENT_SECRET are set') + return False with open(path, 'rb') as f: files = {'file': (os.path.basename(path), f, settings.INSIGHTS_AGENT_MIME)} @@ -377,25 +389,13 @@ def ship(path): s.headers = get_awx_http_client_headers() s.headers.pop('Content-Type') with set_environ(**settings.AWX_TASK_ENV): - if rh_user and rh_password: - try: - client = OIDCClient(rh_user, rh_password, DEFAULT_OIDC_TOKEN_ENDPOINT, ['api.console']) - response = client.make_request("POST", url, headers=s.headers, files=files, verify=settings.INSIGHTS_CERT_PATH, timeout=(31, 31)) - except requests.RequestException: - logger.error("Automation Analytics API request failed, trying base auth method") - response = s.post(url, files=files, verify=settings.INSIGHTS_CERT_PATH, auth=(rh_user, rh_password), headers=s.headers, timeout=(31, 31)) - elif not rh_user or not rh_password: - logger.info('REDHAT_USERNAME and REDHAT_PASSWORD are not set, using SUBSCRIPTIONS_USERNAME and SUBSCRIPTIONS_PASSWORD') - rh_user = getattr(settings, 'SUBSCRIPTIONS_USERNAME', None) - rh_password = getattr(settings, 'SUBSCRIPTIONS_PASSWORD', None) - if rh_user and rh_password: - response = s.post(url, files=files, verify=settings.INSIGHTS_CERT_PATH, auth=(rh_user, rh_password), headers=s.headers, timeout=(31, 31)) - elif not rh_user: - logger.error('REDHAT_USERNAME and SUBSCRIPTIONS_USERNAME are not set') - return False - elif not rh_password: - logger.error('REDHAT_PASSWORD and SUBSCRIPTIONS_USERNAME are not set') - return False + try: + client = OIDCClient(rh_id, rh_secret) + response = client.make_request("POST", url, headers=s.headers, files=files, verify=settings.INSIGHTS_CERT_PATH, timeout=(31, 31)) + except requests.RequestException: + logger.error("Automation Analytics API request failed, trying base auth method") + response = s.post(url, files=files, verify=settings.INSIGHTS_CERT_PATH, auth=(rh_id, rh_secret), headers=s.headers, timeout=(31, 31)) + # Accept 2XX status_codes if response.status_code >= 300: logger.error('Upload failed with status {}, {}'.format(response.status_code, response.text)) diff --git a/awx/main/conf.py b/awx/main/conf.py index c710913d36..39eb34dfdb 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -124,8 +124,8 @@ register( allow_blank=True, encrypted=False, read_only=False, - label=_('Red Hat customer username'), - help_text=_('This username is used to send data to Automation Analytics'), + label=_('Red Hat Client ID for Analytics'), + help_text=_('Client ID used to send data to Automation Analytics'), category=_('System'), category_slug='system', ) @@ -137,34 +137,34 @@ register( allow_blank=True, encrypted=True, read_only=False, - label=_('Red Hat customer password'), - help_text=_('This password is used to send data to Automation Analytics'), + label=_('Red Hat Client Secret for Analytics'), + help_text=_('Client secret used to send data to Automation Analytics'), category=_('System'), category_slug='system', ) register( - 'SUBSCRIPTIONS_USERNAME', + 'SUBSCRIPTIONS_CLIENT_ID', field_class=fields.CharField, default='', allow_blank=True, encrypted=False, read_only=False, - label=_('Red Hat or Satellite username'), - help_text=_('This username is used to retrieve subscription and content information'), # noqa + label=_('Red Hat Client ID for Subscriptions'), + help_text=_('Client ID used to retrieve subscription and content information'), # noqa category=_('System'), category_slug='system', ) register( - 'SUBSCRIPTIONS_PASSWORD', + 'SUBSCRIPTIONS_CLIENT_SECRET', field_class=fields.CharField, default='', allow_blank=True, encrypted=True, read_only=False, - label=_('Red Hat or Satellite password'), - help_text=_('This password is used to retrieve subscription and content information'), # noqa + label=_('Red Hat Client Secret for Subscriptions'), + help_text=_('Client secret used to retrieve subscription and content information'), # noqa category=_('System'), category_slug='system', ) diff --git a/awx/main/tests/functional/analytics/test_core.py b/awx/main/tests/functional/analytics/test_core.py index 67f66900a6..de71f7d043 100644 --- a/awx/main/tests/functional/analytics/test_core.py +++ b/awx/main/tests/functional/analytics/test_core.py @@ -87,8 +87,8 @@ def mock_analytic_post(): { 'REDHAT_USERNAME': 'redhat_user', 'REDHAT_PASSWORD': 'redhat_pass', # NOSONAR - 'SUBSCRIPTIONS_USERNAME': '', - 'SUBSCRIPTIONS_PASSWORD': '', + 'SUBSCRIPTIONS_CLIENT_ID': '', + 'SUBSCRIPTIONS_CLIENT_SECRET': '', }, True, ('redhat_user', 'redhat_pass'), @@ -98,8 +98,8 @@ def mock_analytic_post(): { 'REDHAT_USERNAME': None, 'REDHAT_PASSWORD': None, - 'SUBSCRIPTIONS_USERNAME': 'subs_user', - 'SUBSCRIPTIONS_PASSWORD': 'subs_pass', # NOSONAR + 'SUBSCRIPTIONS_CLIENT_ID': 'subs_user', + 'SUBSCRIPTIONS_CLIENT_SECRET': 'subs_pass', # NOSONAR }, True, ('subs_user', 'subs_pass'), @@ -109,8 +109,8 @@ def mock_analytic_post(): { 'REDHAT_USERNAME': '', 'REDHAT_PASSWORD': '', - 'SUBSCRIPTIONS_USERNAME': 'subs_user', - 'SUBSCRIPTIONS_PASSWORD': 'subs_pass', # NOSONAR + 'SUBSCRIPTIONS_CLIENT_ID': 'subs_user', + 'SUBSCRIPTIONS_CLIENT_SECRET': 'subs_pass', # NOSONAR }, True, ('subs_user', 'subs_pass'), @@ -120,8 +120,8 @@ def mock_analytic_post(): { 'REDHAT_USERNAME': '', 'REDHAT_PASSWORD': '', - 'SUBSCRIPTIONS_USERNAME': '', - 'SUBSCRIPTIONS_PASSWORD': '', + 'SUBSCRIPTIONS_CLIENT_ID': '', + 'SUBSCRIPTIONS_CLIENT_SECRET': '', }, False, None, # No request should be made @@ -131,8 +131,8 @@ def mock_analytic_post(): { 'REDHAT_USERNAME': '', 'REDHAT_PASSWORD': 'redhat_pass', # NOSONAR - 'SUBSCRIPTIONS_USERNAME': 'subs_user', - 'SUBSCRIPTIONS_PASSWORD': '', + 'SUBSCRIPTIONS_CLIENT_ID': 'subs_user', + 'SUBSCRIPTIONS_CLIENT_SECRET': '', }, False, None, # Invalid, no request should be made diff --git a/awx/main/tests/functional/api/test_analytics.py b/awx/main/tests/functional/api/test_analytics.py index 1902ec4811..c1eb7d32ae 100644 --- a/awx/main/tests/functional/api/test_analytics.py +++ b/awx/main/tests/functional/api/test_analytics.py @@ -97,8 +97,8 @@ class TestAnalyticsGenericView: 'INSIGHTS_TRACKING_STATE': True, 'REDHAT_USERNAME': 'redhat_user', 'REDHAT_PASSWORD': 'redhat_pass', # NOSONAR - 'SUBSCRIPTIONS_USERNAME': '', - 'SUBSCRIPTIONS_PASSWORD': '', + 'SUBSCRIPTIONS_CLIENT_ID': '', + 'SUBSCRIPTIONS_CLIENT_SECRET': '', }, ('redhat_user', 'redhat_pass'), None, @@ -109,8 +109,8 @@ class TestAnalyticsGenericView: 'INSIGHTS_TRACKING_STATE': True, 'REDHAT_USERNAME': '', 'REDHAT_PASSWORD': '', - 'SUBSCRIPTIONS_USERNAME': 'subs_user', - 'SUBSCRIPTIONS_PASSWORD': 'subs_pass', # NOSONAR + 'SUBSCRIPTIONS_CLIENT_ID': 'subs_user', + 'SUBSCRIPTIONS_CLIENT_SECRET': 'subs_pass', # NOSONAR }, ('subs_user', 'subs_pass'), None, @@ -121,8 +121,8 @@ class TestAnalyticsGenericView: 'INSIGHTS_TRACKING_STATE': True, 'REDHAT_USERNAME': '', 'REDHAT_PASSWORD': '', - 'SUBSCRIPTIONS_USERNAME': '', - 'SUBSCRIPTIONS_PASSWORD': '', + 'SUBSCRIPTIONS_CLIENT_ID': '', + 'SUBSCRIPTIONS_CLIENT_SECRET': '', }, None, ERROR_MISSING_USER, @@ -133,8 +133,8 @@ class TestAnalyticsGenericView: 'INSIGHTS_TRACKING_STATE': True, 'REDHAT_USERNAME': 'redhat_user', 'REDHAT_PASSWORD': 'redhat_pass', # NOSONAR - 'SUBSCRIPTIONS_USERNAME': 'subs_user', - 'SUBSCRIPTIONS_PASSWORD': 'subs_pass', # NOSONAR + 'SUBSCRIPTIONS_CLIENT_ID': 'subs_user', + 'SUBSCRIPTIONS_CLIENT_SECRET': 'subs_pass', # NOSONAR }, ('redhat_user', 'redhat_pass'), None, @@ -145,8 +145,8 @@ class TestAnalyticsGenericView: 'INSIGHTS_TRACKING_STATE': True, 'REDHAT_USERNAME': '', 'REDHAT_PASSWORD': '', - 'SUBSCRIPTIONS_USERNAME': 'subs_user', # NOSONAR - 'SUBSCRIPTIONS_PASSWORD': '', + 'SUBSCRIPTIONS_CLIENT_ID': 'subs_user', # NOSONAR + 'SUBSCRIPTIONS_CLIENT_SECRET': '', }, None, ERROR_MISSING_PASSWORD, diff --git a/awx/main/tests/unit/utils/test_licensing.py b/awx/main/tests/unit/utils/test_licensing.py new file mode 100644 index 0000000000..0a93aa8612 --- /dev/null +++ b/awx/main/tests/unit/utils/test_licensing.py @@ -0,0 +1,37 @@ +import json +from http import HTTPStatus +from unittest.mock import patch + +from requests import Response + +from awx.main.utils.licensing import Licenser + + +def test_rhsm_licensing(): + def mocked_requests_get(*args, **kwargs): + assert kwargs['verify'] == True + response = Response() + subs = json.dumps({'body': []}) + response.status_code = HTTPStatus.OK + response._content = bytes(subs, 'utf-8') + return response + + licenser = Licenser() + with patch('awx.main.utils.analytics_proxy.OIDCClient.make_request', new=mocked_requests_get): + subs = licenser.get_rhsm_subs('localhost', 'admin', 'admin') + assert subs == [] + + +def test_satellite_licensing(): + def mocked_requests_get(*args, **kwargs): + assert kwargs['verify'] == True + response = Response() + subs = json.dumps({'results': []}) + response.status_code = HTTPStatus.OK + response._content = bytes(subs, 'utf-8') + return response + + licenser = Licenser() + with patch('requests.get', new=mocked_requests_get): + subs = licenser.get_satellite_subs('localhost', 'admin', 'admin') + assert subs == [] diff --git a/awx/main/utils/analytics_proxy.py b/awx/main/utils/analytics_proxy.py index 6e3219b326..a6a599942e 100644 --- a/awx/main/utils/analytics_proxy.py +++ b/awx/main/utils/analytics_proxy.py @@ -23,7 +23,7 @@ class TokenError(requests.RequestException): try: client = OIDCClient(...) client.make_request(...) - except TokenGenerationError as e: + except TokenError as e: print(f"Token generation failed due to {e.__cause__}") except requests.RequestException: print("API request failed) @@ -102,13 +102,15 @@ class OIDCClient: self, client_id: str, client_secret: str, - token_url: str, - scopes: list[str], + token_url: str = DEFAULT_OIDC_TOKEN_ENDPOINT, + scopes: list[str] = None, base_url: str = '', ) -> None: self.client_id: str = client_id self.client_secret: str = client_secret self.token_url: str = token_url + if scopes is None: + scopes = ['api.console'] self.scopes = scopes self.base_url: str = base_url self.token: Optional[Token] = None diff --git a/awx/main/utils/licensing.py b/awx/main/utils/licensing.py index 24109cd39c..3df63c3178 100644 --- a/awx/main/utils/licensing.py +++ b/awx/main/utils/licensing.py @@ -38,6 +38,7 @@ from django.utils.translation import gettext_lazy as _ from awx_plugins.interfaces._temporary_private_licensing_api import detect_server_product_name from awx.main.constants import SUBSCRIPTION_USAGE_MODEL_UNIQUE_HOSTS +from awx.main.utils.analytics_proxy import OIDCClient MAX_INSTANCES = 9999999 @@ -228,37 +229,38 @@ class Licenser(object): host = getattr(settings, 'REDHAT_CANDLEPIN_HOST', None) if not user: - raise ValueError('subscriptions_username is required') + raise ValueError('subscriptions_client_id is required') if not pw: - raise ValueError('subscriptions_password is required') + raise ValueError('subscriptions_client_secret is required') if host and user and pw: if 'subscription.rhsm.redhat.com' in host: - json = self.get_rhsm_subs(host, user, pw) + json = self.get_rhsm_subs(settings.SUBSCRIPTIONS_RHSM_URL, user, pw) else: json = self.get_satellite_subs(host, user, pw) return self.generate_license_options_from_entitlements(json) return [] - def get_rhsm_subs(self, host, user, pw): - verify = getattr(settings, 'REDHAT_CANDLEPIN_VERIFY', True) - json = [] - try: - subs = requests.get('/'.join([host, 'subscription/users/{}/owners'.format(user)]), verify=verify, auth=(user, pw)) - except requests.exceptions.ConnectionError as error: - raise error - except OSError as error: - raise OSError( - 'Unable to open certificate bundle {}. Check that the service is running on Red Hat Enterprise Linux.'.format(verify) - ) from error # noqa - subs.raise_for_status() + def get_rhsm_subs(self, host, client_id, client_secret): + client = OIDCClient(client_id, client_secret) + subs = client.make_request( + 'GET', + host, + verify=True, + timeout=(31, 31), + ) - for sub in subs.json(): - resp = requests.get('/'.join([host, 'subscription/owners/{}/pools/?match=*tower*'.format(sub['key'])]), verify=verify, auth=(user, pw)) - resp.raise_for_status() - json.extend(resp.json()) - return json + subs.raise_for_status() + subs_formatted = [] + for sku in subs.json()['body']: + sku_data = {k: v for k, v in sku.items() if k != 'subscriptions'} + for sub in sku['subscriptions']: + sub_data = sku_data.copy() + sub_data['subscriptions'] = sub + subs_formatted.append(sub_data) + + return subs_formatted def get_satellite_subs(self, host, user, pw): port = None @@ -267,7 +269,7 @@ class Licenser(object): port = str(self.config.get("server", "port")) except Exception as e: logger.exception('Unable to read rhsm config to get ca_cert location. {}'.format(str(e))) - verify = getattr(settings, 'REDHAT_CANDLEPIN_VERIFY', True) + verify = True if port: host = ':'.join([host, port]) json = [] @@ -314,20 +316,11 @@ class Licenser(object): return False return True - def is_appropriate_sub(self, sub): - if sub['activeSubscription'] is False: - return False - # Products that contain Ansible Tower - products = sub.get('providedProducts', []) - if any(product.get('productId') == '480' for product in products): - return True - return False - def generate_license_options_from_entitlements(self, json): from dateutil.parser import parse ValidSub = collections.namedtuple( - 'ValidSub', 'sku name support_level end_date trial developer_license quantity pool_id satellite subscription_id account_number usage' + 'ValidSub', 'sku name support_level end_date trial developer_license quantity satellite subscription_id account_number usage' ) valid_subs = [] for sub in json: @@ -335,10 +328,14 @@ class Licenser(object): if satellite: is_valid = self.is_appropriate_sat_sub(sub) else: - is_valid = self.is_appropriate_sub(sub) + # the list of subs from console.redhat.com are already valid based on the query params we provided + is_valid = True if is_valid: try: - end_date = parse(sub.get('endDate')) + if satellite: + end_date = parse(sub.get('endDate')) + else: + end_date = parse(sub['subscriptions']['endDate']) except Exception: continue now = datetime.utcnow() @@ -346,44 +343,50 @@ class Licenser(object): if end_date < now: # If the sub has a past end date, skip it continue - try: - quantity = int(sub['quantity']) - if quantity == -1: - # effectively, unlimited - quantity = MAX_INSTANCES - except Exception: - continue - sku = sub['productId'] - trial = sku.startswith('S') # i.e.,, SER/SVC developer_license = False support_level = '' - usage = '' - pool_id = sub['id'] - subscription_id = sub['subscriptionId'] - account_number = sub['accountNumber'] + account_number = '' + usage = sub.get('usage', '') if satellite: + try: + quantity = int(sub['quantity']) + except Exception: + continue + sku = sub['productId'] + subscription_id = sub['subscriptionId'] + sub_name = sub['productName'] support_level = sub['support_level'] - usage = sub['usage'] + account_number = sub['accountNumber'] else: - for attr in sub.get('productAttributes', []): - if attr.get('name') == 'support_level': - support_level = attr.get('value') - elif attr.get('name') == 'usage': - usage = attr.get('value') - elif attr.get('name') == 'ph_product_name' and attr.get('value') == 'RHEL Developer': - developer_license = True + try: + if sub['capacity']['name'] == "Nodes": + quantity = int(sub['capacity']['quantity']) * int(sub['subscriptions']['quantity']) + else: + continue + except Exception: + continue + sku = sub['sku'] + sub_name = sub['name'] + support_level = sub['serviceLevel'] + subscription_id = sub['subscriptions']['number'] + if sub.get('name') == 'RHEL Developer': + developer_license = True + + if quantity == -1: + # effectively, unlimited + quantity = MAX_INSTANCES + trial = sku.startswith('S') # i.e.,, SER/SVC valid_subs.append( ValidSub( sku, - sub['productName'], + sub_name, support_level, end_date, trial, developer_license, quantity, - pool_id, satellite, subscription_id, account_number, @@ -414,7 +417,6 @@ class Licenser(object): license._attrs['satellite'] = satellite license._attrs['valid_key'] = True license.update(license_date=int(sub.end_date.strftime('%s'))) - license.update(pool_id=sub.pool_id) license.update(subscription_id=sub.subscription_id) license.update(account_number=sub.account_number) licenses.append(license._attrs.copy()) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d2e5ba4839..533f49c935 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -964,6 +964,9 @@ CLUSTER_HOST_ID = socket.gethostname() # - 'unique_managed_hosts': Compliant = automated - deleted hosts (using /api/v2/host_metrics/) SUBSCRIPTION_USAGE_MODEL = '' +# Default URL and query params for obtaining valid AAP subscriptions +SUBSCRIPTIONS_RHSM_URL = 'https://console.redhat.com/api/rhsm/v2/products?include=providedProducts&oids=480&status=Active' + # Host metrics cleanup - last time of the task/command run CLEANUP_HOST_METRICS_LAST_TS = None # Host metrics cleanup - minimal interval between two cleanups in days diff --git a/awx_collection/plugins/modules/license.py b/awx_collection/plugins/modules/license.py index 06f02a821d..5885e3f5de 100644 --- a/awx_collection/plugins/modules/license.py +++ b/awx_collection/plugins/modules/license.py @@ -31,9 +31,9 @@ options: unlicensed or trial licensed. When force=true, the license is always applied. type: bool default: 'False' - pool_id: + subscription_id: description: - - Red Hat or Red Hat Satellite pool_id to attach to + - Red Hat or Red Hat Satellite subscription_id to attach to required: False type: str state: @@ -57,9 +57,9 @@ EXAMPLES = ''' username: "my_satellite_username" password: "my_satellite_password" -- name: Attach to a pool (requires fetching subscriptions at least once before) +- name: Attach to a subscription (requires fetching subscriptions at least once before) license: - pool_id: 123456 + subscription_id: 123456 - name: Remove license license: @@ -75,14 +75,14 @@ def main(): module = ControllerAPIModule( argument_spec=dict( manifest=dict(type='str', required=False), - pool_id=dict(type='str', required=False), + subscription_id=dict(type='str', required=False), force=dict(type='bool', default=False), state=dict(choices=['present', 'absent'], default='present'), ), required_if=[ - ['state', 'present', ['manifest', 'pool_id'], True], + ['state', 'present', ['manifest', 'subscription_id'], True], ], - mutually_exclusive=[("manifest", "pool_id")], + mutually_exclusive=[("manifest", "subscription_id")], ) json_output = {'changed': False} @@ -124,7 +124,7 @@ def main(): if module.params.get('manifest', None): module.post_endpoint('config', data={'manifest': manifest.decode()}) else: - module.post_endpoint('config/attach', data={'pool_id': module.params.get('pool_id')}) + module.post_endpoint('config/attach', data={'subscription_id': module.params.get('subscription_id')}) module.exit_json(**json_output) diff --git a/awx_collection/plugins/modules/subscriptions.py b/awx_collection/plugins/modules/subscriptions.py index 0f89e71ded..761fb9cc3c 100644 --- a/awx_collection/plugins/modules/subscriptions.py +++ b/awx_collection/plugins/modules/subscriptions.py @@ -20,15 +20,15 @@ description: - Get subscriptions available to Automation Platform Controller. See U(https://www.ansible.com/tower) for an overview. options: - username: + client_id: description: - - Red Hat or Red Hat Satellite username to get available subscriptions. + - Red Hat service account client ID or Red Hat Satellite username to get available subscriptions. - The credentials you use will be stored for future use in retrieving renewal or expanded subscriptions required: True type: str - password: + client_secret: description: - - Red Hat or Red Hat Satellite password to get available subscriptions. + - Red Hat service account client secret or Red Hat Satellite password to get available subscriptions. - The credentials you use will be stored for future use in retrieving renewal or expanded subscriptions required: True type: str @@ -53,13 +53,13 @@ subscriptions: EXAMPLES = ''' - name: Get subscriptions subscriptions: - username: "my_username" - password: "My Password" + client_id: "c6bd7594-d776-46e5-8156-6d17af147479" + client_secret: "MO9QUvoOZ5fc5JQKXoTch1AsTLI7nFsZ" - name: Get subscriptions with a filter subscriptions: - username: "my_username" - password: "My Password" + client_id: "c6bd7594-d776-46e5-8156-6d17af147479" + client_secret: "MO9QUvoOZ5fc5JQKXoTch1AsTLI7nFsZ" filters: product_name: "Red Hat Ansible Automation Platform" support_level: "Self-Support" @@ -72,8 +72,8 @@ def main(): module = ControllerAPIModule( argument_spec=dict( - username=dict(type='str', required=True), - password=dict(type='str', no_log=True, required=True), + client_id=dict(type='str', required=True), + client_secret=dict(type='str', no_log=True, required=True), filters=dict(type='dict', required=False, default={}), ), ) @@ -82,8 +82,8 @@ def main(): # Check if Tower is already licensed post_data = { - 'subscriptions_password': module.params.get('password'), - 'subscriptions_username': module.params.get('username'), + 'subscriptions_client_secret': module.params.get('client_secret'), + 'subscriptions_client_id': module.params.get('client_id'), } all_subscriptions = module.post_endpoint('config/subscriptions', data=post_data)['json'] json_output['subscriptions'] = []