mirror of
https://github.com/ansible/awx.git
synced 2026-04-14 06:29:25 -02:30
reimplement licensing to work with RHSM and entitlement uploads
Co-authored-by: Christian Adams <chadams@redhat.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -153,6 +153,7 @@ from awx.api.views.root import ( # noqa
|
||||
ApiV2PingView,
|
||||
ApiV2ConfigView,
|
||||
ApiV2SubscriptionView,
|
||||
ApiV2AttachView,
|
||||
)
|
||||
from awx.api.views.webhooks import ( # noqa
|
||||
WebhookKeyView,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
20
awx/conf/migrations/0008_subscriptions.py
Normal file
20
awx/conf/migrations/0008_subscriptions.py
Normal 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),
|
||||
]
|
||||
17
awx/conf/migrations/_subscriptions.py
Normal file
17
awx/conf/migrations/_subscriptions.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
382
awx/main/utils/licensing.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -667,6 +667,10 @@ EC2_ENABLED_VALUE = 'running'
|
||||
EC2_INSTANCE_ID_VAR = 'ec2_id'
|
||||
EC2_EXCLUDE_EMPTY_GROUPS = True
|
||||
|
||||
# Entitlements
|
||||
ENTITLEMENT_CERT = ''
|
||||
|
||||
|
||||
# ------------
|
||||
# -- VMware --
|
||||
# ------------
|
||||
|
||||
@@ -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 {};
|
||||
});
|
||||
|
||||
@@ -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}) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 > 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
|
||||
|
||||
@@ -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 {};
|
||||
});
|
||||
|
||||
@@ -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} );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user