diff --git a/.gitignore b/.gitignore index 7e0f07a83c..3b0fbc7d59 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,4 @@ use_dev_supervisor.txt .idea/* *.unison.tmp *.# +/tools/docker-compose/overrides/ diff --git a/Makefile b/Makefile index ed73f4a5b6..8d891c25b5 100644 --- a/Makefile +++ b/Makefile @@ -650,9 +650,11 @@ awx/projects: docker-compose-isolated: awx/projects CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-isolated-override.yml up +COMPOSE_UP_OPTS ?= "" + # Docker Compose Development environment docker-compose: docker-auth awx/projects - CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml up --no-recreate awx + CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml $(COMPOSE_UP_OPTS) up --no-recreate awx docker-compose-cluster: docker-auth awx/projects CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose-cluster.yml up diff --git a/awx/api/generics.py b/awx/api/generics.py index fce5bb9b49..0c6f2637f6 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -47,8 +47,6 @@ from awx.main.utils import ( get_object_or_400, decrypt_field, get_awx_version, - get_licenser, - StubLicense ) from awx.main.utils.db import get_all_field_names from awx.main.views import ApiErrorView @@ -225,7 +223,8 @@ class APIView(views.APIView): response = super(APIView, self).finalize_response(request, response, *args, **kwargs) time_started = getattr(self, 'time_started', None) response['X-API-Product-Version'] = get_awx_version() - response['X-API-Product-Name'] = 'AWX' if isinstance(get_licenser(), StubLicense) else 'Red Hat Ansible Tower' + response['X-API-Product-Name'] = 'AWX' if settings.LICENSE.get('license_type', 'UNLICENSED') in 'open' else 'Red Hat Ansible Tower' + response['X-API-Node'] = settings.CLUSTER_HOST_ID if time_started: time_elapsed = time.time() - self.time_started diff --git a/awx/api/parsers.py b/awx/api/parsers.py index ce18bce0af..8c9cfede94 100644 --- a/awx/api/parsers.py +++ b/awx/api/parsers.py @@ -34,3 +34,28 @@ class JSONParser(parsers.JSONParser): return obj except ValueError as exc: raise ParseError(_('JSON parse error - %s\nPossible cause: trailing comma.' % str(exc))) + + +class ConfigJSONParser(parsers.JSONParser): + """ + Entitlement Certificates have newlines in them which require json.loads to + not use strict parsing. + """ + + def parse(self, stream, media_type=None, parser_context=None): + """ + Parses the incoming bytestream as JSON and returns the resulting data. + """ + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + + try: + data = smart_str(stream.read(), encoding=encoding) + if not data: + return {} + obj = json.loads(data, object_pairs_hook=OrderedDict, strict=False) + if not isinstance(obj, dict) and obj is not None: + raise ParseError(_('JSON parse error - not a JSON object')) + return obj + except ValueError as exc: + raise ParseError(_('JSON parse error - %s\nPossible cause: trailing comma.' % str(exc))) diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 2745f87095..636e68e4bd 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -15,6 +15,7 @@ from awx.api.views import ( ApiV2PingView, ApiV2ConfigView, ApiV2SubscriptionView, + ApiV2AttachView, AuthView, UserMeList, DashboardView, @@ -94,6 +95,7 @@ v2_urls = [ url(r'^ping/$', ApiV2PingView.as_view(), name='api_v2_ping_view'), url(r'^config/$', ApiV2ConfigView.as_view(), name='api_v2_config_view'), url(r'^config/subscriptions/$', ApiV2SubscriptionView.as_view(), name='api_v2_subscription_view'), + url(r'^config/attach/$', ApiV2AttachView.as_view(), name='api_v2_attach_view'), url(r'^auth/$', AuthView.as_view()), url(r'^me/$', UserMeList.as_view(), name='user_me_list'), url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 3b5ffc9671..df138b8a6d 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -153,6 +153,7 @@ from awx.api.views.root import ( # noqa ApiV2PingView, ApiV2ConfigView, ApiV2SubscriptionView, + ApiV2AttachView, ) from awx.api.views.webhooks import ( # noqa WebhookKeyView, diff --git a/awx/api/views/root.py b/awx/api/views/root.py index aeda19cdeb..abcd0eed8a 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -1,9 +1,9 @@ # Copyright (c) 2018 Ansible, Inc. # All Rights Reserved. +import json import logging import operator -import json from collections import OrderedDict from django.conf import settings @@ -20,6 +20,7 @@ from rest_framework import status import requests from awx.api.generics import APIView +from awx.api.parsers import ConfigJSONParser from awx.conf.registry import settings_registry from awx.main.analytics import all_collectors from awx.main.ha import is_ha_environment @@ -30,7 +31,6 @@ from awx.main.utils import ( to_python_boolean, ) from awx.api.versioning import reverse, drf_reverse -from awx.conf.license import get_license from awx.main.constants import PRIVILEGE_ESCALATION_METHODS from awx.main.models import ( Project, @@ -178,7 +178,7 @@ class ApiV2PingView(APIView): class ApiV2SubscriptionView(APIView): permission_classes = (IsAuthenticated,) - name = _('Configuration') + name = _('Subscriptions') swagger_topic = 'System Configuration' def check_permissions(self, request): @@ -189,16 +189,16 @@ class ApiV2SubscriptionView(APIView): def post(self, request): from awx.main.utils.common import get_licenser data = request.data.copy() - if data.get('rh_password') == '$encrypted$': - data['rh_password'] = settings.REDHAT_PASSWORD + if data.get('subscriptions_password') == '$encrypted$': + data['subscriptions_password'] = settings.SUBSCRIPTIONS_PASSWORD try: - user, pw = data.get('rh_username'), data.get('rh_password') + user, pw = data.get('subscriptions_username'), data.get('subscriptions_password') with set_environ(**settings.AWX_TASK_ENV): validated = get_licenser().validate_rh(user, pw) if user: - settings.REDHAT_USERNAME = data['rh_username'] + settings.SUBSCRIPTIONS_USERNAME = data['subscriptions_username'] if pw: - settings.REDHAT_PASSWORD = data['rh_password'] + settings.SUBSCRIPTIONS_PASSWORD = data['subscriptions_password'] except Exception as exc: msg = _("Invalid License") if ( @@ -220,11 +220,63 @@ class ApiV2SubscriptionView(APIView): return Response(validated) +class ApiV2AttachView(APIView): + + permission_classes = (IsAuthenticated,) + name = _('Attach Subscription') + swagger_topic = 'System Configuration' + + def check_permissions(self, request): + super(ApiV2AttachView, self).check_permissions(request) + if not request.user.is_superuser and request.method.lower() not in {'options', 'head'}: + self.permission_denied(request) # Raises PermissionDenied exception. + + def post(self, request): + data = request.data.copy() + pool_id = data.get('pool_id', None) + # org = data.get('org', None) # if we want allow to user to specify the org, we will need to pass this + 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: + from awx.main.utils.common import get_licenser + data = request.data.copy() + try: + with set_environ(**settings.AWX_TASK_ENV): + validated = get_licenser().validate_rh(user, pw) + except Exception as exc: + msg = _("Invalid License") + if ( + 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.") + elif isinstance(exc, requests.exceptions.ConnectionError): + msg = _("Could not connect to subscription service.") + elif isinstance(exc, (ValueError, OSError)) and exc.args: + msg = exc.args[0] + else: + logger.exception(smart_text(u"Invalid license 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: + sub['valid_key'] = True + settings.LICENSE = sub + return Response(sub) + + return Response({"error": _("Error processing subscription metadata.")}, status=status.HTTP_400_BAD_REQUEST) + + class ApiV2ConfigView(APIView): permission_classes = (IsAuthenticated,) name = _('Configuration') swagger_topic = 'System Configuration' + parser_classes = (ConfigJSONParser,) def check_permissions(self, request): super(ApiV2ConfigView, self).check_permissions(request) @@ -234,15 +286,11 @@ class ApiV2ConfigView(APIView): def get(self, request, format=None): '''Return various sitewide configuration settings''' - if request.user.is_superuser or request.user.is_system_auditor: - license_data = get_license(show_key=True) - else: - license_data = get_license(show_key=False) + from awx.main.utils.common import get_licenser + license_data = get_licenser().validate(new_cert=False) + if not license_data.get('valid_key', False): license_data = {} - if license_data and 'features' in license_data and 'activity_streams' in license_data['features']: - # FIXME: Make the final setting value dependent on the feature? - license_data['features']['activity_streams'] &= settings.ACTIVITY_STREAM_ENABLED pendo_state = settings.PENDO_TRACKING_STATE if settings.PENDO_TRACKING_STATE in ('off', 'anonymous', 'detailed') else 'off' @@ -281,6 +329,7 @@ class ApiV2ConfigView(APIView): return Response(data) + def post(self, request): if not isinstance(request.data, dict): return Response({"error": _("Invalid license data")}, status=status.HTTP_400_BAD_REQUEST) @@ -300,18 +349,26 @@ class ApiV2ConfigView(APIView): logger.info(smart_text(u"Invalid JSON submitted for license."), extra=dict(actor=request.user.username)) return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST) + + # Save Entitlement Cert/Key + license_data = json.loads(data_actual) + if 'entitlement_cert' in license_data: + settings.ENTITLEMENT_CERT = license_data['entitlement_cert'] + try: + # Validate entitlement cert and get subscription metadata + # validate() will clear the entitlement cert if not valid from awx.main.utils.common import get_licenser - license_data = json.loads(data_actual) - license_data_validated = get_licenser(**license_data).validate() + license_data_validated = get_licenser().validate(new_cert=True) except Exception: logger.warning(smart_text(u"Invalid license submitted."), extra=dict(actor=request.user.username)) + # If License invalid, clear entitlment cert value + settings.ENTITLEMENT_CERT = '' return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST) # If the license is valid, write it to the database. if license_data_validated['valid_key']: - settings.LICENSE = license_data if not settings_registry.is_setting_read_only('TOWER_URL_BASE'): settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host()) return Response(license_data_validated) @@ -321,9 +378,12 @@ class ApiV2ConfigView(APIView): return Response({"error": _("Invalid license")}, status=status.HTTP_400_BAD_REQUEST) def delete(self, request): + # Clear license and entitlement certificate try: settings.LICENSE = {} + settings.ENTITLEMENT_CERT = '' return Response(status=status.HTTP_204_NO_CONTENT) except Exception: # FIX: Log return Response({"error": _("Failed to remove license.")}, status=status.HTTP_400_BAD_REQUEST) + diff --git a/awx/conf/license.py b/awx/conf/license.py index 6ad1042f9a..b9d142a9f1 100644 --- a/awx/conf/license.py +++ b/awx/conf/license.py @@ -1,18 +1,14 @@ # Copyright (c) 2016 Ansible, Inc. # All Rights Reserved. - __all__ = ['get_license'] def _get_validated_license_data(): - from awx.main.utils.common import get_licenser - return get_licenser().validate() + from awx.main.utils.licensing import Licenser + return Licenser().validate(new_cert=False) -def get_license(show_key=False): +def get_license(): """Return a dictionary representing the active license on this Tower instance.""" - license_data = _get_validated_license_data() - if not show_key: - license_data.pop('license_key', None) - return license_data + return _get_validated_license_data() diff --git a/awx/conf/migrations/0008_subscriptions.py b/awx/conf/migrations/0008_subscriptions.py new file mode 100644 index 0000000000..8b3dff6cf4 --- /dev/null +++ b/awx/conf/migrations/0008_subscriptions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.11 on 2020-08-04 15:19 + +import logging + +from django.db import migrations + +from awx.conf.migrations import _subscriptions as subscriptions + +logger = logging.getLogger('awx.conf.migrations') + + +class Migration(migrations.Migration): + + dependencies = [ + ('conf', '0007_v380_rename_more_settings'), + ] + + operations = [ + migrations.RunPython(subscriptions.clear_old_license), + ] diff --git a/awx/conf/migrations/_subscriptions.py b/awx/conf/migrations/_subscriptions.py new file mode 100644 index 0000000000..b28c8dd195 --- /dev/null +++ b/awx/conf/migrations/_subscriptions.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +import logging +from django.conf import settings + +from awx.main.utils.licensing import Licenser + +logger = logging.getLogger('awx.conf.settings') + +__all__ = ['clear_old_license'] + + +def clear_old_license(apps, schema_editor): + # Setting = apps.get_model('conf', 'Organization') + # setting.objects.filter(key=LICENSE) + licenser = Licenser() + if licenser._check_product_cert(): + settings.LICENSE = licenser.UNLICENSED_DATA.copy() diff --git a/awx/conf/models.py b/awx/conf/models.py index 2859650f54..fe28fd89a8 100644 --- a/awx/conf/models.py +++ b/awx/conf/models.py @@ -78,14 +78,6 @@ class Setting(CreatedModifiedModel): def get_cache_id_key(self, key): return '{}_ID'.format(key) - def display_value(self): - if self.key == 'LICENSE' and 'license_key' in self.value: - # don't log the license key in activity stream - value = self.value.copy() - value['license_key'] = '********' - return value - return self.value - import awx.conf.signals # noqa diff --git a/awx/main/access.py b/awx/main/access.py index 0ecb025d92..536c8e4699 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -307,7 +307,7 @@ class BaseAccess(object): return True # User has access to both, permission check passed def check_license(self, add_host_name=None, feature=None, check_expiration=True, quiet=False): - validation_info = get_licenser().validate() + validation_info = get_licenser().validate(new_cert=False) if validation_info.get('license_type', 'UNLICENSED') == 'open': return @@ -345,7 +345,7 @@ class BaseAccess(object): report_violation(_("Host count exceeds available instances.")) def check_org_host_limit(self, data, add_host_name=None): - validation_info = get_licenser().validate() + validation_info = get_licenser().validate(new_cert=False) if validation_info.get('license_type', 'UNLICENSED') == 'open': return diff --git a/awx/main/analytics/collectors.py b/awx/main/analytics/collectors.py index e1dc468d51..bcbc7b0118 100644 --- a/awx/main/analytics/collectors.py +++ b/awx/main/analytics/collectors.py @@ -33,9 +33,9 @@ data _since_ the last report date - i.e., new data in the last 24 hours) ''' -@register('config', '1.1', description=_('General platform configuration.')) +@register('config', '1.2', description=_('General platform configuration.')) def config(since, **kwargs): - license_info = get_license(show_key=False) + license_info = get_license() install_type = 'traditional' if os.environ.get('container') == 'oci': install_type = 'openshift' diff --git a/awx/main/analytics/core.py b/awx/main/analytics/core.py index fe48fb30bf..2c77444929 100644 --- a/awx/main/analytics/core.py +++ b/awx/main/analytics/core.py @@ -24,7 +24,7 @@ logger = logging.getLogger('awx.main.analytics') def _valid_license(): try: - if get_license(show_key=False).get('license_type', 'UNLICENSED') == 'open': + if get_license().get('license_type', 'UNLICENSED') == 'open': return False access_registry[Job](None).check_license() except PermissionDenied: diff --git a/awx/main/analytics/metrics.py b/awx/main/analytics/metrics.py index 1dd85eb6a7..676ccdabf5 100644 --- a/awx/main/analytics/metrics.py +++ b/awx/main/analytics/metrics.py @@ -54,7 +54,7 @@ LICENSE_INSTANCE_FREE = Gauge('awx_license_instance_free', 'Number of remaining def metrics(): - license_info = get_license(show_key=False) + license_info = get_license() SYSTEM_INFO.info({ 'install_uuid': settings.INSTALL_UUID, 'insights_analytics': str(settings.INSIGHTS_TRACKING_STATE), diff --git a/awx/main/conf.py b/awx/main/conf.py index 3b41c3a19b..95e15e9def 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -12,6 +12,8 @@ from rest_framework.fields import FloatField # Tower from awx.conf import fields, register, register_validate +from awx.main.validators import validate_entitlement_cert + logger = logging.getLogger('awx.main.conf') @@ -142,6 +144,32 @@ register( category_slug='system', ) +register( + 'SUBSCRIPTIONS_USERNAME', + 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 + category=_('System'), + category_slug='system', +) + +register( + 'SUBSCRIPTIONS_PASSWORD', + 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 + category=_('System'), + category_slug='system', +) + register( 'AUTOMATION_ANALYTICS_URL', field_class=fields.URLField, @@ -328,6 +356,21 @@ register( category_slug='jobs', ) +register( + 'ENTITLEMENT_CERT', + field_class=fields.CharField, + allow_blank=True, + default='', + required=False, + validators=[validate_entitlement_cert], # TODO: may need to use/modify `validate_certificate` validator + label=_('RHSM Entitlement Public Certificate and Private Key'), + help_text=_('Obtain a key pair via subscription-manager, or https://access.redhat.com. Refer to Ansible Tower docs for formatting key pair.'), + category=_('SYSTEM'), + category_slug='system', + encrypted=True, +) + + register( 'AWX_RESOURCE_PROFILING_ENABLED', field_class=fields.BooleanField, diff --git a/awx/main/management/commands/check_license.py b/awx/main/management/commands/check_license.py index 8c0798cc53..23d51c7e17 100644 --- a/awx/main/management/commands/check_license.py +++ b/awx/main/management/commands/check_license.py @@ -16,9 +16,7 @@ class Command(BaseCommand): def handle(self, *args, **options): super(Command, self).__init__() - license = get_licenser().validate() + license = get_licenser().validate(new_cert=False) if options.get('data'): - if license.get('license_key', '') != 'UNLICENSED': - license['license_key'] = '********' return json.dumps(license) return license.get('license_type', 'none') diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index f4431b2705..9f97faf45e 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -901,7 +901,7 @@ class Command(BaseCommand): )) def check_license(self): - license_info = get_licenser().validate() + license_info = get_licenser().validate(new_cert=False) local_license_type = license_info.get('license_type', 'UNLICENSED') if license_info.get('license_key', 'UNLICENSED') == 'UNLICENSED': logger.error(LICENSE_NON_EXISTANT_MESSAGE) @@ -938,7 +938,7 @@ class Command(BaseCommand): logger.warning(LICENSE_MESSAGE % d) def check_org_host_limit(self): - license_info = get_licenser().validate() + license_info = get_licenser().validate(new_cert=False) if license_info.get('license_type', 'UNLICENSED') == 'open': return diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 19669a0f27..2a1c85e23d 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -2160,7 +2160,7 @@ class RunProjectUpdate(BaseTask): 'local_path': os.path.basename(project_update.project.local_path), 'project_path': project_update.get_project_path(check_if_exists=False), # deprecated 'insights_url': settings.INSIGHTS_URL_BASE, - 'awx_license_type': get_license(show_key=False).get('license_type', 'UNLICENSED'), + 'awx_license_type': get_license().get('license_type', 'UNLICENSED'), 'awx_version': get_awx_version(), 'scm_url': scm_url, 'scm_branch': scm_branch, diff --git a/awx/main/tests/functional/core/test_licenses.py b/awx/main/tests/functional/core/test_licenses.py index f59318502c..ba8b1ff9af 100644 --- a/awx/main/tests/functional/core/test_licenses.py +++ b/awx/main/tests/functional/core/test_licenses.py @@ -1,13 +1,16 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. -from awx.main.utils.common import StubLicense +# from awx.main.utils.common import StubLicense +# +# +# def test_stub_license(): +# license_actual = StubLicense().validate() +# assert license_actual['license_key'] == 'OPEN' +# assert license_actual['valid_key'] +# assert license_actual['compliant'] +# assert license_actual['license_type'] == 'open' -def test_stub_license(): - license_actual = StubLicense().validate() - assert license_actual['license_key'] == 'OPEN' - assert license_actual['valid_key'] - assert license_actual['compliant'] - assert license_actual['license_type'] == 'open' +# Test license_date is always seconds diff --git a/awx/main/tests/functional/test_licenses.py b/awx/main/tests/functional/test_licenses.py index 6c34321f8d..f3e623d281 100644 --- a/awx/main/tests/functional/test_licenses.py +++ b/awx/main/tests/functional/test_licenses.py @@ -30,8 +30,7 @@ def test_python_and_js_licenses(): # Check variations of '-' and '_' in filenames due to python for fname in [name, name.replace('-','_')]: if entry.startswith(fname) and entry.endswith('.tar.gz'): - entry = entry[:-7] - (n, v) = entry.rsplit('-',1) + v = entry.split(name + '-')[1].split('.tar.gz')[0] return v return None diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index b49af2efd0..93ef41a190 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -39,6 +39,8 @@ from awx.main import tasks from awx.main.utils import encrypt_field, encrypt_value from awx.main.utils.safe_yaml import SafeLoader +from awx.main.utils.licensing import Licenser + class TestJobExecution(object): EXAMPLE_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxyz==\n-----END PRIVATE KEY-----' @@ -1830,7 +1832,10 @@ class TestProjectUpdateGalaxyCredentials(TestJobExecution): task = RunProjectUpdate() env = task.build_env(project_update, private_data_dir) - task.build_extra_vars_file(project_update, private_data_dir) + + with mock.patch.object(Licenser, 'validate', lambda *args, **kw: {}): + task.build_extra_vars_file(project_update, private_data_dir) + assert task.__vars__['roles_enabled'] is False assert task.__vars__['collections_enabled'] is False for k in env: @@ -1850,7 +1855,10 @@ class TestProjectUpdateGalaxyCredentials(TestJobExecution): project_update.project.organization.galaxy_credentials.add(public_galaxy) task = RunProjectUpdate() env = task.build_env(project_update, private_data_dir) - task.build_extra_vars_file(project_update, private_data_dir) + + with mock.patch.object(Licenser, 'validate', lambda *args, **kw: {}): + task.build_extra_vars_file(project_update, private_data_dir) + assert task.__vars__['roles_enabled'] is True assert task.__vars__['collections_enabled'] is True assert sorted([ @@ -1935,7 +1943,9 @@ class TestProjectUpdateCredentials(TestJobExecution): assert settings.PROJECTS_ROOT in process_isolation['process_isolation_show_paths'] task._write_extra_vars_file = mock.Mock() - task.build_extra_vars_file(project_update, private_data_dir) + + with mock.patch.object(Licenser, 'validate', lambda *args, **kw: {}): + task.build_extra_vars_file(project_update, private_data_dir) call_args, _ = task._write_extra_vars_file.call_args_list[0] _, extra_vars = call_args diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index a65120c8e8..f628d8f831 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -55,8 +55,7 @@ __all__ = [ 'model_instance_diff', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', 'has_model_field_prefetched', 'set_environ', 'IllegalArgumentError', 'get_custom_venv_choices', 'get_external_account', 'task_manager_bulk_reschedule', - 'schedule_task_manager', 'classproperty', 'create_temporary_fifo', 'truncate_stdout', - 'StubLicense' + 'schedule_task_manager', 'classproperty', 'create_temporary_fifo', 'truncate_stdout' ] @@ -190,7 +189,7 @@ def get_awx_version(): def get_awx_http_client_headers(): - license = get_license(show_key=False).get('license_type', 'UNLICENSED') + license = get_license().get('license_type', 'UNLICENSED') headers = { 'Content-Type': 'application/json', 'User-Agent': '{} {} ({})'.format( @@ -202,34 +201,14 @@ def get_awx_http_client_headers(): return headers -class StubLicense(object): - - features = { - 'activity_streams': True, - 'ha': True, - 'ldap': True, - 'multiple_organizations': True, - 'surveys': True, - 'system_tracking': True, - 'rebranding': True, - 'enterprise_auth': True, - 'workflows': True, - } - - def validate(self): - return dict(license_key='OPEN', - valid_key=True, - compliant=True, - features=self.features, - license_type='open') - - +# Update all references of this function in the codebase to just import `from awx.main.utils.licensing import Licenser` directly def get_licenser(*args, **kwargs): try: - from tower_license import TowerLicense - return TowerLicense(*args, **kwargs) - except ImportError: - return StubLicense(*args, **kwargs) + # Get License Config from db? + from awx.main.utils.licensing import Licenser + return Licenser(*args, **kwargs) + except Exception as e: + raise ValueError(_('Error importing Tower License: %s') % e) def update_scm_url(scm_type, url, username=True, password=True, diff --git a/awx/main/utils/licensing.py b/awx/main/utils/licensing.py new file mode 100644 index 0000000000..1021dcbb8e --- /dev/null +++ b/awx/main/utils/licensing.py @@ -0,0 +1,382 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +''' +This is intended to be a lightweight license class for verifying subscriptions, and parsing subscription data +from entitlement certificates. + +The Licenser class can do the following: + - Parse an Entitlement cert to generate license +''' + +import os +import configparser +from datetime import datetime +import collections +import copy +import tempfile +import logging +import re +import requests +import time + +# Django +from django.conf import settings + +# AWX +from awx.main.models import Host + +# RHSM +from rhsm import certificate + +MAX_INSTANCES = 9999999 + +logger = logging.getLogger(__name__) + + +def rhsm_config(): + config = configparser.ConfigParser() + config.read('/etc/rhsm/rhsm.conf') + return config + + +class Licenser(object): + # warn when there is a month (30 days) left on the license + LICENSE_TIMEOUT = 60 * 60 * 24 * 30 + + UNLICENSED_DATA = dict( + subscription_name=None, + sku=None, + support_level=None, + instance_count=0, + license_date=0, + license_type="UNLICENSED", + product_name="Red Hat Ansible Tower", + valid_key=False + ) + + def __init__(self, **kwargs): + self._attrs = dict( + instance_count=0, + license_date=0, + license_type='UNLICENSED', + ) + self.config = rhsm_config() + if not kwargs: + license_setting = getattr(settings, 'LICENSE', None) + if license_setting is not None: + kwargs = license_setting + + if 'company_name' in kwargs: + kwargs.pop('company_name') + self._attrs.update(kwargs) + if self._check_product_cert(): + if 'license_key' in self._attrs: + self._unset_attrs() + if 'valid_key' in self._attrs: + if not self._attrs['valid_key']: + self._unset_attrs() + else: + self._unset_attrs() + else: + self._generate_open_config() + + + def _check_product_cert(self): + # Product Cert Name: ansible-tower-3.7-rhel-7.x86_64.pem + # Maybe check validity of Product Cert somehow? + if os.path.exists('/etc/tower/certs') and os.path.exists('/var/lib/awx/.tower_version'): + return True + return False + + + def _generate_open_config(self): + self._attrs.update(dict(license_type='open', + valid_key=True, + subscription_name='OPEN', + product_name="AWX", + )) + settings.LICENSE = self._attrs + + + def _unset_attrs(self): + self._attrs = self.UNLICENSED_DATA.copy() + + + def _clear_license_setting(self): + self.unset_attrs() + settings.LICENSE = {} + + + def _generate_product_config(self): + raw_cert = getattr(settings, 'ENTITLEMENT_CERT', None) + # Fail early if no entitlement cert is available + if not raw_cert or raw_cert == '': + self._clear_license_setting() + return + + # Read certificate + certinfo = certificate.create_from_pem(raw_cert) + if not certinfo.is_valid(): + raise ValueError("Could not parse entitlement certificate") + if certinfo.is_expired(): + raise ValueError("Certificate is expired") + if not any(map(lambda x: x.id == '480', certinfo.products)): + self._clear_license_setting() + raise ValueError("Certificate is for another product") + + # Parse output for subscription metadata to build config + license = dict() + license['sku'] = certinfo.order.sku + license['instance_count'] = int(certinfo.order.quantity_used) + license['support_level'] = certinfo.order.service_level + license['subscription_name'] = certinfo.order.name + license['pool_id'] = certinfo.pool.id + license['license_date'] = certinfo.end.strftime('%s') + license['product_name'] = certinfo.order.name + license['valid_key'] = True + license['license_type'] = 'enterprise' + license['satellite'] = False + + self._attrs.update(license) + settings.LICENSE = self._attrs + return self._attrs + + + def update(self, **kwargs): + # Update attributes of the current license. + if 'instance_count' in kwargs: + kwargs['instance_count'] = int(kwargs['instance_count']) + if 'license_date' in kwargs: + kwargs['license_date'] = int(kwargs['license_date']) + self._attrs.update(kwargs) + + + def validate_rh(self, user, pw): + host = 'https://' + str(self.config.get("server", "hostname")) + if not host: + host = getattr(settings, 'REDHAT_CANDLEPIN_HOST', None) + + if not user: + raise ValueError('subscriptions_username is required') + + if not pw: + raise ValueError('subscriptions_password is required') + + if host and user and pw: + if 'subscription.rhsm.redhat.com' in host: + json = self.get_rhsm_subs(host, 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', False) + 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 Ansible Tower is running on Red Hat Enterprise Linux.'.format(verify)) from error # noqa + subs.raise_for_status() + + 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 + + + def get_satellite_subs(self, host, user, pw): + try: + verify = str(self.config.get("rhsm", "repo_ca_cert")) + except Exception as e: + raise OSError('Unable to read rhsm config to get ca_cert location. {}'.format(str(e))) + json = [] + try: + orgs = requests.get( + '/'.join([host, 'katello/api/organizations']), + 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 Ansible Tower is running on Red Hat Enterprise Linux.'.format(verify)) from error # noqa + orgs.raise_for_status() + + for org in orgs.json()['results']: + resp = requests.get( + '/'.join([ + host, + '/katello/api/organizations/{}/subscriptions/?search=Red Hat Ansible Automation'.format(org['id']) + ]), + verify=verify, + auth=(user, pw) + ) + resp.raise_for_status() + results = resp.json()['results'] + if results != []: + for sub in results: + # Parse output for subscription metadata to build config + license = dict() + license['productId'] = sub['product_id'] + license['quantity'] = int(sub['quantity']) + license['support_level'] = sub['support_level'] + license['subscription_name'] = sub['name'] + license['id'] = sub['upstream_pool_id'] + license['endDate'] = sub['end_date'] + license['productName'] = "Red Hat Ansible Automation" + license['valid_key'] = True + license['license_type'] = 'enterprise' + license['satellite'] = True + json.append(license) + return json + + + def is_appropriate_sat_sub(self, sub): + if 'Red Hat Ansible Automation' not in sub['subscription_name']: + 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(map(lambda product: product.get('productId', None) == "480", 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 quantity pool_id satellite') + valid_subs = [] + for sub in json: + satellite = sub['satellite'] + if satellite: + is_valid = self.is_appropriate_sat_sub(sub) + else: + is_valid = self.is_appropriate_sub(sub) + if is_valid: + try: + end_date = parse(sub.get('endDate')) + except Exception: + continue + now = datetime.utcnow() + now = now.replace(tzinfo=end_date.tzinfo) + 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 + support_level = '' + pool_id = sub['id'] + if satellite: + support_level = sub['support_level'] + else: + for attr in sub.get('productAttributes', []): + if attr.get('name') == 'support_level': + support_level = attr.get('value') + + valid_subs.append(ValidSub( + sku, sub['productName'], support_level, end_date, trial, quantity, pool_id, satellite + )) + + if valid_subs: + licenses = [] + for sub in valid_subs: + license = self.__class__(subscription_name='Ansible Tower by Red Hat') + license._attrs['instance_count'] = int(sub.quantity) + license._attrs['sku'] = sub.sku + license._attrs['support_level'] = sub.support_level + license._attrs['license_type'] = 'enterprise' + if sub.trial: + license._attrs['trial'] = True + license._attrs['instance_count'] = min( + MAX_INSTANCES, license._attrs['instance_count'] + ) + human_instances = license._attrs['instance_count'] + if human_instances == MAX_INSTANCES: + human_instances = 'Unlimited' + subscription_name = re.sub( + r' \([\d]+ Managed Nodes', + ' ({} Managed Nodes'.format(human_instances), + sub.name + ) + license._attrs['subscription_name'] = subscription_name + license._attrs['satellite'] = satellite + license.update( + license_date=int(sub.end_date.strftime('%s')) + ) + license.update( + pool_id=sub.pool_id + ) + licenses.append(license._attrs.copy()) + return licenses + + raise ValueError( + 'No valid Red Hat Ansible Automation subscription could be found for this account.' # noqa + ) + + + def validate(self, new_cert=False): + + # Generate Config from Entitlement cert if it exists + if new_cert: + self._generate_product_config() + + # Return license attributes with additional validation info. + attrs = copy.deepcopy(self._attrs) + type = attrs.get('license_type', 'none') + + if (type == 'UNLICENSED' or False): + attrs.update(dict(valid_key=False, compliant=False)) + return attrs + attrs['valid_key'] = True + + if Host: + current_instances = Host.objects.active_count() + else: + current_instances = 0 + available_instances = int(attrs.get('instance_count', None) or 0) + attrs['current_instances'] = current_instances + attrs['available_instances'] = available_instances + attrs['free_instances'] = max(0, available_instances - current_instances) + + license_date = int(attrs.get('license_date', None) or 0) + current_date = int(time.time()) + time_remaining = license_date - current_date + attrs['time_remaining'] = time_remaining + if attrs.setdefault('trial', False): + attrs['grace_period_remaining'] = time_remaining + else: + attrs['grace_period_remaining'] = (license_date + 2592000) - current_date + attrs['compliant'] = bool(time_remaining > 0 and attrs['free_instances'] >= 0) + attrs['date_warning'] = bool(time_remaining < self.LICENSE_TIMEOUT) + attrs['date_expired'] = bool(time_remaining <= 0) + return attrs diff --git a/awx/main/validators.py b/awx/main/validators.py index 879be056e5..d751b51cbe 100644 --- a/awx/main/validators.py +++ b/awx/main/validators.py @@ -17,6 +17,12 @@ from rest_framework.exceptions import ParseError from awx.main.utils import parse_yaml_or_json +def validate_entitlement_cert(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None): + # TODO: replace with more actual robust logic here to allow for multiple certificates in one cert file (because this is how entitlements do) + # This is a temporary hack that should not be merged. Look at Ln:92 + pass + + def validate_pem(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None): """ Validate the given PEM data is valid and contains the required numbers of diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 618f5282a4..707f40aa00 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -667,6 +667,10 @@ EC2_ENABLED_VALUE = 'running' EC2_INSTANCE_ID_VAR = 'ec2_id' EC2_EXCLUDE_EMPTY_GROUPS = True +# Entitlements +ENTITLEMENT_CERT = '' + + # ------------ # -- VMware -- # ------------ diff --git a/awx/ui/client/src/configuration/forms/settings-form.route.js b/awx/ui/client/src/configuration/forms/settings-form.route.js index 20ca06fd7f..ea0d04b7e4 100644 --- a/awx/ui/client/src/configuration/forms/settings-form.route.js +++ b/awx/ui/client/src/configuration/forms/settings-form.route.js @@ -54,20 +54,20 @@ export default { }); }], resolve: { - rhCreds: ['Rest', 'GetBasePath', function(Rest, GetBasePath) { + subscriptionCreds: ['Rest', 'GetBasePath', function(Rest, GetBasePath) { Rest.setUrl(`${GetBasePath('settings')}system/`); return Rest.get() .then(({data}) => { - const rhCreds = {}; + const subscriptionCreds = {}; if (data.REDHAT_USERNAME && data.REDHAT_USERNAME !== "") { - rhCreds.REDHAT_USERNAME = data.REDHAT_USERNAME; + subscriptionCreds.REDHAT_USERNAME = data.REDHAT_USERNAME; } if (data.REDHAT_PASSWORD && data.REDHAT_PASSWORD !== "") { - rhCreds.REDHAT_PASSWORD = data.REDHAT_PASSWORD; + subscriptionCreds.REDHAT_PASSWORD = data.REDHAT_PASSWORD; } - return rhCreds; + return subscriptionCreds; }).catch(() => { return {}; }); diff --git a/awx/ui/client/src/license/checkLicense.factory.js b/awx/ui/client/src/license/checkLicense.factory.js index e76f1ea0b7..73fdc5e15e 100644 --- a/awx/ui/client/src/license/checkLicense.factory.js +++ b/awx/ui/client/src/license/checkLicense.factory.js @@ -15,14 +15,26 @@ export default return config.license_info; }, - post: function(payload, eula){ - var defaultUrl = GetBasePath('config'); + post: function(payload, eula, attach){ + var defaultUrl = GetBasePath('config') + (attach ? 'attach/' : ''); Rest.setUrl(defaultUrl); var data = payload; - data.eula_accepted = eula; + + if (!attach) { + data.eula_accepted = eula; + } return Rest.post(JSON.stringify(data)) .then((response) =>{ + if (attach) { + var configPayload = {}; + configPayload.eula_accepted = eula; + Rest.setUrl(GetBasePath('config')); + return Rest.post(configPayload) + .then((configResponse) => { + return configResponse.data; + }); + } return response.data; }) .catch(({data}) => { diff --git a/awx/ui/client/src/license/license.controller.js b/awx/ui/client/src/license/license.controller.js index 37c1c38364..00d00565ab 100644 --- a/awx/ui/client/src/license/license.controller.js +++ b/awx/ui/client/src/license/license.controller.js @@ -8,9 +8,9 @@ import {N_} from "../i18n"; export default ['Wait', '$state', '$scope', '$rootScope', 'ProcessErrors', 'CheckLicense', 'moment', '$timeout', 'Rest', 'LicenseStrings', - '$window', 'ConfigService', 'pendoService', 'insightsEnablementService', 'i18n', 'config', 'rhCreds', 'GetBasePath', + '$window', 'ConfigService', 'pendoService', 'insightsEnablementService', 'i18n', 'config', 'subscriptionCreds', 'GetBasePath', function(Wait, $state, $scope, $rootScope, ProcessErrors, CheckLicense, moment, $timeout, Rest, LicenseStrings, - $window, ConfigService, pendoService, insightsEnablementService, i18n, config, rhCreds, GetBasePath) { + $window, ConfigService, pendoService, insightsEnablementService, i18n, config, subscriptionCreds, GetBasePath) { $scope.strings = LicenseStrings; @@ -35,7 +35,7 @@ export default const reset = function() { $scope.newLicense.eula = undefined; - $scope.rhCreds = {}; + $scope.subscriptionCreds = {}; $scope.selectedLicense = {}; }; @@ -44,9 +44,9 @@ export default $scope.fileName = N_("No file selected."); if ($rootScope.licenseMissing) { - $scope.title = $rootScope.BRAND_NAME + i18n._(" License"); + $scope.title = $rootScope.BRAND_NAME + i18n._(" Subscription"); } else { - $scope.title = i18n._("License Management"); + $scope.title = i18n._("Subscription Management"); } $scope.license = config; @@ -62,32 +62,41 @@ export default insights: true }; - $scope.rhCreds = {}; + $scope.subscriptionCreds = {}; - if (rhCreds.REDHAT_USERNAME && rhCreds.REDHAT_USERNAME !== "") { - $scope.rhCreds.username = rhCreds.REDHAT_USERNAME; + if (subscriptionCreds.SUBSCRIPTIONS_USERNAME && subscriptionCreds.SUBSCRIPTIONS_USERNAME !== "") { + $scope.subscriptionCreds.username = subscriptionCreds.SUBSCRIPTIONS_USERNAME; } - if (rhCreds.REDHAT_PASSWORD && rhCreds.REDHAT_PASSWORD !== "") { - $scope.rhCreds.password = rhCreds.REDHAT_PASSWORD; + if (subscriptionCreds.SUBSCRIPTIONS_PASSWORD && subscriptionCreds.SUBSCRIPTIONS_PASSWORD !== "") { + $scope.subscriptionCreds.password = subscriptionCreds.SUBSCRIPTIONS_PASSWORD; + $scope.showPlaceholderPassword = true; + } + + if (subscriptionCreds.ORGANIZATION_ID && subscriptionCreds.ORGANIZATION_ID !== "") { + $scope.subscriptionCreds.organization_id = subscriptionCreds.ORGANIZATION_ID; $scope.showPlaceholderPassword = true; } }; - const updateRHCreds = (config) => { + const updateSubscriptionCreds = (config) => { Rest.setUrl(`${GetBasePath('settings')}system/`); Rest.get() .then(({data}) => { initVars(config); - if (data.REDHAT_USERNAME && data.REDHAT_USERNAME !== "") { - $scope.rhCreds.username = data.REDHAT_USERNAME; + if (data.SUBSCRIPTIONS_USERNAME && data.SUBSCRIPTIONS_USERNAME !== "") { + $scope.subscriptionCreds.username = data.SUBSCRIPTIONS_USERNAME; } - if (data.REDHAT_PASSWORD && data.REDHAT_PASSWORD !== "") { - $scope.rhCreds.password = data.REDHAT_PASSWORD; + if (data.SUBSCRIPTIONS_PASSWORD && data.SUBSCRIPTIONS_PASSWORD !== "") { + $scope.subscriptionCreds.password = data.SUBSCRIPTIONS_PASSWORD; $scope.showPlaceholderPassword = true; } + + if (data.ENTITLEMENT_CONSUMER && data.ENTITLEMENT_CONSUMER.org && data.ENTITLEMENT_CONSUMER.org !== "") { + $scope.subscriptionCreds.organization_id = data.ENTITLEMENT_CONSUMER.org; + } }).catch(() => { initVars(config); }); @@ -100,28 +109,23 @@ export default $scope.fileName = event.target.files[0].name; // Grab the key from the raw license file const raw = new FileReader(); - // readAsFoo runs async + raw.onload = function() { - try { - $scope.newLicense.file = JSON.parse(raw.result); - } catch(err) { - ProcessErrors($rootScope, null, null, null, - {msg: i18n._('Invalid file format. Please upload valid JSON.')}); - } + $scope.newLicense.entitlement_cert = raw.result; }; try { raw.readAsText(event.target.files[0]); } catch(err) { ProcessErrors($rootScope, null, null, null, - {msg: i18n._('Invalid file format. Please upload valid JSON.')}); + {msg: i18n._('Invalid file format. Please upload a certificate/key pair')}); } }; // HTML5 spec doesn't provide a way to customize file input css // So we hide the default input, show our own, and simulate clicks to the hidden input $scope.fakeClick = function() { - if($scope.user_is_superuser && (!$scope.rhCreds.username || $scope.rhCreds.username === '') && (!$scope.rhCreds.password || $scope.rhCreds.password === '')) { + if($scope.user_is_superuser && (!$scope.subscriptionCreds.username || $scope.subscriptionCreds.username === '') && (!$scope.subscriptionCreds.password || $scope.subscriptionCreds.password === '')) { $('#License-file').click(); } }; @@ -131,9 +135,9 @@ export default }; $scope.replacePassword = () => { - if ($scope.user_is_superuser && !$scope.newLicense.file) { + if ($scope.user_is_superuser && !$scope.newLicense.entitlement_cert) { $scope.showPlaceholderPassword = false; - $scope.rhCreds.password = ""; + $scope.subscriptionCreds.password = ""; $timeout(() => { $('.tooltip').remove(); $('#rh-password').focus(); @@ -142,9 +146,9 @@ export default }; $scope.lookupLicenses = () => { - if ($scope.rhCreds.username && $scope.rhCreds.password) { + if ($scope.subscriptionCreds.username && $scope.subscriptionCreds.password) { Wait('start'); - ConfigService.getSubscriptions($scope.rhCreds.username, $scope.rhCreds.password) + ConfigService.getSubscriptions($scope.subscriptionCreds.username, $scope.subscriptionCreds.password) .then(({data}) => { Wait('stop'); if (data && data.length > 0) { @@ -172,29 +176,31 @@ export default $scope.confirmLicenseSelection = () => { $scope.showLicenseModal = false; $scope.selectedLicense.fullLicense = $scope.rhLicenses.find((license) => { - return license.license_key === $scope.selectedLicense.modalKey; + return license.pool_id === $scope.selectedLicense.modalPoolId; }); - $scope.selectedLicense.modalKey = undefined; + $scope.selectedLicense.modalPoolId = undefined; }; $scope.cancelLicenseLookup = () => { $scope.showLicenseModal = false; - $scope.selectedLicense.modalKey = undefined; + $scope.selectedLicense.modalPoolId = undefined; }; $scope.submit = function() { Wait('start'); let payload = {}; - if ($scope.newLicense.file) { - payload = $scope.newLicense.file; + let attach = false; + if ($scope.newLicense.entitlement_cert) { + payload.entitlement_cert = $scope.newLicense.entitlement_cert; } else if ($scope.selectedLicense.fullLicense) { - payload = $scope.selectedLicense.fullLicense; + payload.pool_id = $scope.selectedLicense.fullLicense.pool_id; + payload.org = $scope.subscriptionCreds.organization_id; + attach = true; } - CheckLicense.post(payload, $scope.newLicense.eula) - .then((licenseInfo) => { + CheckLicense.post(payload, $scope.newLicense.eula, attach) + .finally((licenseInfo) => { reset(); - ConfigService.delete(); ConfigService.getConfig(licenseInfo) .then(function(config) { @@ -217,7 +223,7 @@ export default licenseMissing: false }); } else { - updateRHCreds(config); + updateSubscriptionCreds(config); $scope.success = true; $rootScope.licenseMissing = false; // for animation purposes diff --git a/awx/ui/client/src/license/license.partial.html b/awx/ui/client/src/license/license.partial.html index 2dc7beeb9c..6958a536e0 100644 --- a/awx/ui/client/src/license/license.partial.html +++ b/awx/ui/client/src/license/license.partial.html @@ -5,10 +5,10 @@