reimplement licensing to work with RHSM and entitlement uploads

Co-authored-by: Christian Adams <chadams@redhat.com>
This commit is contained in:
Ryan Petrello
2020-10-22 10:56:26 -04:00
parent a26d20cbf2
commit ffab48c77f
42 changed files with 1117 additions and 174 deletions

View File

@@ -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

View File

@@ -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)))

View File

@@ -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'),

View File

@@ -153,6 +153,7 @@ from awx.api.views.root import ( # noqa
ApiV2PingView,
ApiV2ConfigView,
ApiV2SubscriptionView,
ApiV2AttachView,
)
from awx.api.views.webhooks import ( # noqa
WebhookKeyView,

View File

@@ -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)

View File

@@ -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()

View File

@@ -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),
]

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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:

View File

@@ -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),

View File

@@ -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,

View File

@@ -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')

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

382
awx/main/utils/licensing.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -667,6 +667,10 @@ EC2_ENABLED_VALUE = 'running'
EC2_INSTANCE_ID_VAR = 'ec2_id'
EC2_EXCLUDE_EMPTY_GROUPS = True
# Entitlements
ENTITLEMENT_CERT = ''
# ------------
# -- VMware --
# ------------

View File

@@ -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 {};
});

View File

@@ -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}) => {

View File

@@ -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

View File

@@ -5,10 +5,10 @@
<div class="List-titleText" translate>Details</div>
<div class="License-fields">
<div class="License-field">
<div class="License-field--label" translate>License</div>
<div class="License-field--label" translate>Subscription</div>
<div class="License-field--content">
<span class="License-greenText" ng-show='compliant'><i class="fa fa-circle License-greenText"></i><translate>Valid License</translate></span>
<span class="License-redText" ng-show='compliant !== undefined && !compliant'><i class="fa fa-circle License-redText"></i><translate>Invalid License</translate></span>
<span class="License-greenText" ng-show='compliant'><i class="fa fa-circle License-greenText"></i><translate>Valid</translate></span>
<span class="License-redText" ng-show='compliant !== undefined && !compliant'><i class="fa fa-circle License-redText"></i><translate>Invalid</translate></span>
</div>
</div>
<div class="License-field">
@@ -18,7 +18,7 @@
</div>
</div>
<div class="License-field">
<div class="License-field--label" translate>License Type</div>
<div class="License-field--label" translate>Subscription Type</div>
<div class="License-field--content">
{{license.license_info.license_type}}
</div>
@@ -29,12 +29,6 @@
{{license.license_info.subscription_name}}
</div>
</div>
<div class="License-field">
<div class="License-field--label" translate>License Key</div>
<div class="License-field--content License-field--key">
{{license.license_info.license_key}}
</div>
</div>
<div class="License-field">
<div class="License-field--label" translate>Expires On</div>
<div class="License-field--content">
@@ -79,7 +73,7 @@
<div class="card at-Panel">
<div class="List-titleText">{{title}}</div>
<div class="License-body">
<div class="License-helperText License-introText" ng-if="licenseMissing" translate>Welcome to Ansible Tower! Please complete the steps below to acquire a license.</div>
<div class="License-helperText License-introText" ng-if="licenseMissing" translate>Welcome to Ansible Tower! Please complete the steps below to acquire a subscription.</div>
<div class="input-group License-file--container">
<div class="License-file--left">
<div class="d-block w-100">
@@ -88,12 +82,12 @@
1
</span>
<span class="License-helperText">
<translate>Please click the button below to visit Ansible's website to get a Tower license key.</translate>
<translate>Please click the button below to visit Ansible's website to get a Tower subscription.</translate>
</span>
</div>
<button class="License-downloadLicenseButton btn btn-primary" ng-if="licenseMissing" ng-click="downloadLicense()">
<translate>Request License</translate>
<translate>Request Subscription</translate>
</button>
<div class="AddPermissions-directions">
@@ -101,16 +95,16 @@
2
</span>
<span class="License-helperText">
<translate>Choose your license file, agree to the End User License Agreement, and click submit.</translate>
<translate>Choose your entitlement certificate and key pair, agree to the End User License Agreement, and click submit.</translate>
</span>
</div>
<div class="License-subTitleText">
<span class="Form-requiredAsterisk">*</span>
<translate>License</translate>
<translate>Entitlement certificate and key</translate>
</div>
<div class="License-helperText License-licenseStepHelp" translate>Upload a license file</div>
<div class="License-helperText License-licenseStepHelp" translate>Upload an entitlement certificate and key pair</div>
<div class="License-filePicker">
<span class="btn btn-primary" ng-click="fakeClick()" ng-disabled="!user_is_superuser || (rhCreds.username && rhCreds.username.length > 0) || (rhCreds.password && rhCreds.password.length > 0)" translate>Browse</span>
<span class="btn btn-primary" ng-click="fakeClick()" ng-disabled="!user_is_superuser || (subscriptionCreds.username && subscriptionCreds.username.length > 0) || (subscriptionCreds.password && subscriptionCreds.password.length > 0)" translate>Browse</span>
<span class="License-fileName" ng-class="{'License-helperText' : fileName == 'No file selected.'}">{{fileName|translate}}</span>
<input id="License-file" class="form-control" type="file" file-on-change="getKey"/>
</div>
@@ -125,12 +119,13 @@
<div class="d-block w-100">
<div class="AddPermissions-directions">
<span class="License-helperText">
<translate>Provide your Red Hat customer credentials and you can choose from a list of your available licenses. The credentials you use will be stored for future use in retrieving renewal or expanded licenses. You can update or remove them in SETTINGS &gt; SYSTEM.</translate>
<<<<<<< HEAD
<translate>Provide your Red Hat or Satellite credentials below and you can choose from a list of your available subscriptions. If you are a Satellite user please specify the organization ID you wish to associate this Tower Installation to. The credentials you use will be stored for future use in retrieving renewal or expanded subscriptions.</translate>
</span>
</div>
<div class="License-rhCredField">
<label class="License-label d-block" translate>USERNAME</label>
<input class="form-control Form-textInput" type="text" ng-model="rhCreds.username" ng-disabled="!user_is_superuser || newLicense.file" />
<input class="form-control Form-textInput" type="text" ng-model="subscriptionCreds.username" ng-disabled="!user_is_superuser || newLicense.file" />
</div>
<div class="License-rhCredField">
<label class="License-label d-block" translate>PASSWORD</label>
@@ -143,11 +138,15 @@
</span>
</div>
<div class="input-group" ng-if="!showPlaceholderPassword">
<input id="rh-password" class="form-control Form-textInput" type="password" ng-model="rhCreds.password" ng-disabled="!user_is_superuser || newLicense.file" />
<input id="rh-password" class="form-control Form-textInput" type="password" ng-model="subscriptionCreds.password" ng-disabled="!user_is_superuser || newLicense.file" />
</div>
</div>
<div class="License-rhCredField">
<label class="License-label d-block" translate>(OPTIONAL) ORGANIZATION LABEL</label>
<input class="form-control Form-textInput" type="text" ng-model="subscriptionCreds.organization_id" ng-disabled="!user_is_superuser || newLicense.file" />
</div>
<div class="License-getLicensesButton">
<span ng-click="lookupLicenses()" class="btn btn-primary" ng-disabled="!rhCreds.username || !rhCreds.password" translate>GET LICENSES</button>
<span ng-click="lookupLicenses()" class="btn btn-primary" ng-disabled="!subscriptionCreds.username || !subscriptionCreds.password" translate>GET LICENSES</button>
</div>
<div ng-if="selectedLicense.fullLicense">
<div class="at-RowItem-label" translate>
@@ -200,7 +199,7 @@
<span ng-show="success == true" class="License-greenText License-submit--success pull-right" translate>Save successful!</span>
</div>
<div>
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="(!newLicense.file && !selectedLicense.fullLicense) || (newLicense.file && newLicense.file.license_key == null) || newLicense.eula == null || !user_is_superuser" translate>Submit</button>
<button ng-click="submit()" class="btn btn-success pull-right" ng-disabled="(!newLicense.entitlement_cert && !selectedLicense.fullLicense) || newLicense.eula == null || !user_is_superuser" translate>Submit</button>
</div>
</div>
</div>
@@ -223,12 +222,12 @@
<div class="Modal-body ng-binding">
<div class="License-modalBody">
<form>
<div class="License-modalRow" ng-repeat="license in rhLicenses track by license.license_key">
<div class="License-modalRow" ng-repeat="license in rhLicenses track by license.pool_id">
<div class="License-modalRowRadio">
<input type="radio" id="license-{{license.license_key}}" ng-model="selectedLicense.modalKey" value="{{license.license_key}}" />
<input type="radio" id="license-{{license.pool_id}}" ng-model="selectedLicense.modalPoolId" value="{{license.pool_id}}"/>
</div>
<div class="License-modalRowDetails">
<label for="license-{{license.license_key}}" class="License-modalRowDetailsLabel">
<label for="license-{{license.pool_id}}" class="License-modalRowDetailsLabel">
<div class="License-modalRowDetailsRow">
<div class="License-trialTag" ng-if="license.trial" translate>
Trial
@@ -260,7 +259,7 @@
<button
ng-click="confirmLicenseSelection()"
class="btn Modal-footerButton btn-success"
ng-disabled="!selectedLicense.modalKey"
ng-disabled="!selectedLicense.modalPoolId"
translate
>
SELECT

View File

@@ -15,7 +15,7 @@ export default {
controller: 'licenseController',
data: {},
ncyBreadcrumb: {
label: N_('LICENSE')
label: N_('SUBSCRIPTION')
},
onEnter: ['$state', 'ConfigService', (state, configService) => {
return configService.getConfig()
@@ -43,20 +43,24 @@ export default {
});
}
],
rhCreds: ['Rest', 'GetBasePath', function(Rest, GetBasePath) {
subscriptionCreds: ['Rest', 'GetBasePath', function(Rest, GetBasePath) {
Rest.setUrl(`${GetBasePath('settings')}system/`);
return Rest.get()
.then(({data}) => {
const rhCreds = {};
if (data.REDHAT_USERNAME && data.REDHAT_USERNAME !== "") {
rhCreds.REDHAT_USERNAME = data.REDHAT_USERNAME;
const subscriptionCreds = {};
if (data.SUBSCRIPTIONS_USERNAME && data.SUBSCRIPTIONS_USERNAME !== "") {
subscriptionCreds.SUBSCRIPTIONS_USERNAME = data.SUBSCRIPTIONS_USERNAME;
}
if (data.REDHAT_PASSWORD && data.REDHAT_PASSWORD !== "") {
rhCreds.REDHAT_PASSWORD = data.REDHAT_PASSWORD;
if (data.SUBSCRIPTIONS_PASSWORD && data.SUBSCRIPTIONS_PASSWORD !== "") {
subscriptionCreds.SUBSCRIPTIONS_PASSWORD = data.SUBSCRIPTIONS_PASSWORD;
}
if (data.ENTITLEMENT_CONSUMER && data.ENTITLEMENT_CONSUMER.org && data.ENTITLEMENT_CONSUMER.org !== "") {
subscriptionCreds.ORGANIZATION_ID = data.ENTITLEMENT_CONSUMER.org;
}
return rhCreds;
return subscriptionCreds;
}).catch(() => {
return {};
});

View File

@@ -62,7 +62,7 @@ export default
getSubscriptions: function(username, password) {
Rest.setUrl(`${GetBasePath('config')}subscriptions`);
return Rest.post({ rh_username: username, rh_password: password} );
return Rest.post({ subscriptions_username: username, subscriptions_password: password} );
}
};
}