Merge pull request #8506 from ryanpetrello/yet-another-downstream-merge

merge in some downstream changes

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-10-30 21:15:20 +00:00 committed by GitHub
commit 17b5b531bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 962 additions and 310 deletions

3
.gitignore vendored
View File

@ -34,8 +34,6 @@ awx/ui_next/coverage/
awx/ui_next/build
awx/ui_next/.env.local
rsyslog.pid
/tower-license
/tower-license/**
tools/prometheus/data
tools/docker-compose/Dockerfile
@ -147,3 +145,4 @@ use_dev_supervisor.txt
.idea/*
*.unison.tmp
*.#
/tools/docker-compose/overrides/

View File

@ -650,9 +650,11 @@ awx/projects:
docker-compose-isolated: awx/projects
CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-isolated-override.yml up
COMPOSE_UP_OPTS ?=
# Docker Compose Development environment
docker-compose: docker-auth awx/projects
CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml up --no-recreate awx
CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml $(COMPOSE_UP_OPTS) up --no-recreate awx
docker-compose-cluster: docker-auth awx/projects
CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose-cluster.yml up

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
@ -189,7 +187,8 @@ class APIView(views.APIView):
'''
Log warning for 400 requests. Add header with elapsed time.
'''
from awx.main.utils import get_licenser
from awx.main.utils.licensing import OpenLicense
#
# If the URL was rewritten, and we get a 404, we should entirely
# replace the view in the request context with an ApiErrorView()
@ -225,7 +224,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 isinstance(get_licenser(), OpenLicense) 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

@ -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,10 @@
# Copyright (c) 2018 Ansible, Inc.
# All Rights Reserved.
import base64
import json
import logging
import operator
import json
from collections import OrderedDict
from django.conf import settings
@ -29,8 +30,8 @@ from awx.main.utils import (
get_custom_venv_choices,
to_python_boolean,
)
from awx.main.utils.licensing import validate_entitlement_manifest
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 +179,7 @@ class ApiV2PingView(APIView):
class ApiV2SubscriptionView(APIView):
permission_classes = (IsAuthenticated,)
name = _('Configuration')
name = _('Subscriptions')
swagger_topic = 'System Configuration'
def check_permissions(self, request):
@ -189,18 +190,18 @@ 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")
msg = _("Invalid Subscription")
if (
isinstance(exc, requests.exceptions.HTTPError) and
getattr(getattr(exc, 'response', None), 'status_code', None) == 401
@ -213,13 +214,63 @@ class ApiV2SubscriptionView(APIView):
elif isinstance(exc, (ValueError, OSError)) and exc.args:
msg = exc.args[0]
else:
logger.exception(smart_text(u"Invalid license submitted."),
logger.exception(smart_text(u"Invalid subscription submitted."),
extra=dict(actor=request.user.username))
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
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)
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 Subscription")
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 subscription submitted."),
extra=dict(actor=request.user.username))
return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST)
for sub in validated:
if sub['pool_id'] == pool_id:
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,)
@ -234,15 +285,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()
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,9 +328,10 @@ 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)
return Response({"error": _("Invalid subscription data")}, status=status.HTTP_400_BAD_REQUEST)
if "eula_accepted" not in request.data:
return Response({"error": _("Missing 'eula_accepted' property")}, status=status.HTTP_400_BAD_REQUEST)
try:
@ -300,25 +348,47 @@ 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)
try:
from awx.main.utils.common import get_licenser
license_data = json.loads(data_actual)
license_data_validated = get_licenser(**license_data).validate()
except Exception:
logger.warning(smart_text(u"Invalid license submitted."),
extra=dict(actor=request.user.username))
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
from awx.main.utils.common import get_licenser
license_data = json.loads(data_actual)
if 'license_key' in license_data:
return Response({"error": _('Legacy license submitted. A subscription manifest is now required.')}, status=status.HTTP_400_BAD_REQUEST)
if 'manifest' in license_data:
try:
json_actual = json.loads(base64.b64decode(license_data['manifest']))
if 'license_key' in json_actual:
return Response(
{"error": _('Legacy license submitted. A subscription manifest is now required.')},
status=status.HTTP_400_BAD_REQUEST
)
except Exception:
pass
try:
license_data = validate_entitlement_manifest(license_data['manifest'])
except ValueError as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception:
logger.exception('Invalid manifest submitted. {}')
return Response({"error": _('Invalid manifest submitted.')}, status=status.HTTP_400_BAD_REQUEST)
try:
license_data_validated = get_licenser().license_from_manifest(license_data)
except Exception:
logger.warning(smart_text(u"Invalid subscription submitted."),
extra=dict(actor=request.user.username))
return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST)
else:
license_data_validated = get_licenser().validate()
# 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)
logger.warning(smart_text(u"Invalid license submitted."),
logger.warning(smart_text(u"Invalid subscription submitted."),
extra=dict(actor=request.user.username))
return Response({"error": _("Invalid license")}, status=status.HTTP_400_BAD_REQUEST)
return Response({"error": _("Invalid subscription")}, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request):
try:

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
from awx.main.utils import get_licenser
return get_licenser().validate()
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,26 @@
# Generated by Django 2.2.11 on 2020-08-04 15:19
import logging
from django.db import migrations
from awx.conf.migrations._subscriptions import clear_old_license, prefill_rh_credentials
logger = logging.getLogger('awx.conf.migrations')
def _noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('conf', '0007_v380_rename_more_settings'),
]
operations = [
migrations.RunPython(clear_old_license, _noop),
migrations.RunPython(prefill_rh_credentials, _noop)
]

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
import logging
from django.utils.timezone import now
from awx.main.utils.encryption import decrypt_field, encrypt_field
logger = logging.getLogger('awx.conf.settings')
__all__ = ['clear_old_license', 'prefill_rh_credentials']
def clear_old_license(apps, schema_editor):
Setting = apps.get_model('conf', 'Setting')
Setting.objects.filter(key='LICENSE').delete()
def _migrate_setting(apps, old_key, new_key, encrypted=False):
Setting = apps.get_model('conf', 'Setting')
if not Setting.objects.filter(key=old_key).exists():
return
new_setting = Setting.objects.create(key=new_key,
created=now(),
modified=now()
)
if encrypted:
new_setting.value = decrypt_field(Setting.objects.filter(key=old_key).first(), 'value')
new_setting.value = encrypt_field(new_setting, 'value')
else:
new_setting.value = getattr(Setting.objects.filter(key=old_key).first(), 'value')
new_setting.save()
def prefill_rh_credentials(apps, schema_editor):
_migrate_setting(apps, 'REDHAT_USERNAME', 'SUBSCRIPTIONS_USERNAME', encrypted=False)
_migrate_setting(apps, 'REDHAT_PASSWORD', 'SUBSCRIPTIONS_PASSWORD', encrypted=True)

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

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

@ -1,7 +1,5 @@
# Python
import json
import logging
import os
# Django
from django.utils.translation import ugettext_lazy as _
@ -13,6 +11,7 @@ from rest_framework.fields import FloatField
# Tower
from awx.conf import fields, register, register_validate
logger = logging.getLogger('awx.main.conf')
register(
@ -92,22 +91,10 @@ register(
)
def _load_default_license_from_file():
try:
license_file = os.environ.get('AWX_LICENSE_FILE', '/etc/tower/license')
if os.path.exists(license_file):
license_data = json.load(open(license_file))
logger.debug('Read license data from "%s".', license_file)
return license_data
except Exception:
logger.warning('Could not read license from "%s".', license_file, exc_info=True)
return {}
register(
'LICENSE',
field_class=fields.DictField,
default=_load_default_license_from_file,
default=lambda: {},
label=_('License'),
help_text=_('The license controls which features and functionality are '
'enabled. Use /api/v2/config/ to update or change '
@ -124,7 +111,7 @@ register(
encrypted=False,
read_only=False,
label=_('Red Hat customer username'),
help_text=_('This username is used to retrieve license information and to send Automation Analytics'), # noqa
help_text=_('This username is used to send data to Automation Analytics'),
category=_('System'),
category_slug='system',
)
@ -137,7 +124,33 @@ register(
encrypted=True,
read_only=False,
label=_('Red Hat customer password'),
help_text=_('This password is used to retrieve license information and to send Automation Analytics'), # noqa
help_text=_('This password is used to send data to Automation Analytics'),
category=_('System'),
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',
)

View File

@ -18,7 +18,5 @@ class Command(BaseCommand):
super(Command, self).__init__()
license = get_licenser().validate()
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

@ -903,7 +903,7 @@ class Command(BaseCommand):
def check_license(self):
license_info = get_licenser().validate()
local_license_type = license_info.get('license_type', 'UNLICENSED')
if license_info.get('license_key', 'UNLICENSED') == 'UNLICENSED':
if local_license_type == 'UNLICENSED':
logger.error(LICENSE_NON_EXISTANT_MESSAGE)
raise CommandError('No license found!')
elif local_license_type == 'open':

View File

@ -57,6 +57,7 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase):
def send_messages(self, messages):
sent_messages = 0
self.headers['Content-Type'] = 'application/json'
if 'User-Agent' not in self.headers:
self.headers['User-Agent'] = "Tower {}".format(get_awx_version())
if self.http_method.lower() not in ['put','post']:
@ -68,7 +69,7 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase):
auth = (self.username, self.password)
r = chosen_method("{}".format(m.recipients()[0]),
auth=auth,
json=m.body,
data=json.dumps(m.body, ensure_ascii=False).encode('utf-8'),
headers=self.headers,
verify=(not self.disable_ssl_verification))
if r.status_code >= 400:

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,

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
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'

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,15 @@ 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')
def get_licenser(*args, **kwargs):
from awx.main.utils.licensing import Licenser, OpenLicense
try:
from tower_license import TowerLicense
return TowerLicense(*args, **kwargs)
except ImportError:
return StubLicense(*args, **kwargs)
if os.path.exists('/var/lib/awx/.tower_version'):
return Licenser(*args, **kwargs)
else:
return OpenLicense()
except Exception as e:
raise ValueError(_('Error importing Tower License: %s') % e)
def update_scm_url(scm_type, url, username=True, password=True,

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

@ -0,0 +1,390 @@
# 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 base64
import configparser
from datetime import datetime
import collections
import copy
import io
import json
import logging
import re
import requests
import time
import zipfile
from dateutil.parser import parse as parse_date
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography import x509
# Django
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
# AWX
from awx.main.models import Host
MAX_INSTANCES = 9999999
logger = logging.getLogger(__name__)
def rhsm_config():
path = '/etc/rhsm/rhsm.conf'
config = configparser.ConfigParser()
config.read(path)
return config
def validate_entitlement_manifest(data):
buff = io.BytesIO()
buff.write(base64.b64decode(data))
try:
z = zipfile.ZipFile(buff)
except zipfile.BadZipFile as e:
raise ValueError(_("Invalid manifest: a subscription manifest zip file is required.")) from e
buff = io.BytesIO()
files = z.namelist()
if 'consumer_export.zip' not in files or 'signature' not in files:
raise ValueError(_("Invalid manifest: missing required files."))
export = z.open('consumer_export.zip').read()
sig = z.open('signature').read()
with open('/etc/tower/candlepin-redhat-ca.crt', 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read(), backend=default_backend())
key = cert.public_key()
try:
key.verify(sig, export, padding=padding.PKCS1v15(), algorithm=hashes.SHA256())
except InvalidSignature as e:
raise ValueError(_("Invalid manifest: signature verification failed.")) from e
buff.write(export)
z = zipfile.ZipFile(buff)
for f in z.filelist:
if f.filename.startswith('export/entitlements') and f.filename.endswith('.json'):
return json.loads(z.open(f).read())
raise ValueError(_("Invalid manifest: manifest contains no subscriptions."))
class OpenLicense(object):
def validate(self):
return dict(
license_type='open',
valid_key=True,
subscription_name='OPEN',
product_name="AWX",
)
class Licenser(object):
# warn when there is a month (30 days) left on the subscription
SUBSCRIPTION_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 Automation Platform",
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 'valid_key' in self._attrs:
if not self._attrs['valid_key']:
self._unset_attrs()
else:
self._unset_attrs()
def _unset_attrs(self):
self._attrs = self.UNLICENSED_DATA.copy()
def license_from_manifest(self, manifest):
# Parse output for subscription metadata to build config
license = dict()
license['sku'] = manifest['pool']['productId']
license['instance_count'] = manifest['pool']['quantity']
license['subscription_name'] = manifest['pool']['productName']
license['pool_id'] = manifest['pool']['id']
license['license_date'] = parse_date(manifest['endDate']).strftime('%s')
license['product_name'] = manifest['pool']['productName']
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):
try:
host = 'https://' + str(self.config.get("server", "hostname"))
except Exception:
logger.exception('Cannot access rhsm.conf, make sure subscription manager is installed and configured.')
host = None
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', True)
json = []
try:
subs = requests.get(
'/'.join([host, 'subscription/users/{}/owners'.format(user)]),
verify=verify,
auth=(user, pw)
)
except requests.exceptions.ConnectionError as error:
raise error
except OSError as error:
raise OSError('Unable to open certificate bundle {}. Check that 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:
logger.exception('Unable to read rhsm config to get ca_cert location. {}'.format(str(e)))
verify = getattr(settings, 'REDHAT_CANDLEPIN_VERIFY', True)
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.get('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='Red Hat Ansible Automation Platform')
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['license_type'] = 'trial'
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._attrs['valid_key'] = True
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):
# 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
free_instances = (available_instances - current_instances)
attrs['free_instances'] = max(0, free_instances)
license_date = int(attrs.get('license_date', 0) 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 free_instances >= 0)
attrs['date_warning'] = bool(time_remaining < self.SUBSCRIPTION_TIMEOUT)
attrs['date_expired'] = bool(time_remaining <= 0)
return attrs

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 = {};
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;
}
return rhCreds;
return subscriptionCreds;
}).catch(() => {
return {};
});

View File

@ -139,7 +139,7 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars
else{
$scope.credentialBasePath = (source === 'ec2') ? GetBasePath('credentials') + '?credential_type__namespace=aws' : GetBasePath('credentials') + (source === '' ? '' : '?credential_type__namespace=' + (source));
}
if (source === 'ec2' || source === 'custom' || source === 'vmware' || source === 'openstack' || source === 'scm' || source === 'cloudforms' || source === "satellite6" || source === "azure_rm") {
if (true) {
$scope.envParseType = 'yaml';
var varName;

View File

@ -68,11 +68,7 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange',
multiple: false
});
if (source === 'ec2' || source === 'custom' ||
source === 'vmware' || source === 'openstack' ||
source === 'scm' || source === 'cloudforms' ||
source === 'satellite6' || source === 'azure_rm') {
if (true) {
var varName;
if (source === 'scm') {
varName = 'custom_variables';

View File

@ -174,9 +174,11 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: "<p>" + i18n._("Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables ") +
"<a href=\"https://github.com/ansible-collections/community.aws/blob/main/scripts/inventory/ec2.ini\" target=\"_blank\">" +
i18n._("view ec2.ini in the community.aws repo.") + "</a></p>" +
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.html\" target=\"blank\">aws_ec2</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
@ -198,9 +200,11 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: "<p>" + i18n._("Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables ") +
"<a href=\"https://github.com/ansible-collections/vmware/blob/main/scripts/inventory/vmware_inventory.ini\" target=\"_blank\">" +
i18n._("view vmware_inventory.ini in the vmware community repo.") + "</a></p>" +
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_vm_inventory_inventory.html\" target=\"blank\">vmware_vm_inventory</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
@ -222,9 +226,18 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: i18n._("Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration") +
'<a href=\"https://github.com/openstack/ansible-collections-openstack/blob/master/scripts/inventory/openstack.yml\" target=\"_blank\">' +
i18n._("view openstack.yml in the Openstack github repo.") + "</a>" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."),
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/openstack/cloud/openstack_inventory.html\" target=\"blank\">openstack</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
i18n._("YAML:") + "<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n" +
"<p>" + i18n._("View JSON examples at ") + '<a href="http://www.json.org" target="_blank">www.json.org</a></p>' +
"<p>" + i18n._("View YAML examples at ") + '<a href="http://docs.ansible.com/YAMLSyntax.html" target="_blank">docs.ansible.com</a></p>',
dataContainer: 'body',
subForm: 'sourceSubForm'
},
@ -256,9 +269,18 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: i18n._("Override variables found in foreman.ini and used by the inventory update script. For an example variable configuration") +
'<a href=\"https://github.com/ansible-collections/community.general/blob/main/scripts/inventory/foreman.ini\" target=\"_blank\">' +
i18n._("view foreman.ini in the Ansible Collections github repo.") + "</a>" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax."),
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/theforeman/foreman/foreman_inventory.html\" target=\"blank\">foreman</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
i18n._("YAML:") + "<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n" +
"<p>" + i18n._("View JSON examples at ") + '<a href="http://www.json.org" target="_blank">www.json.org</a></p>' +
"<p>" + i18n._("View YAML examples at ") + '<a href="http://docs.ansible.com/YAMLSyntax.html" target="_blank">docs.ansible.com</a></p>',
dataContainer: 'body',
subForm: 'sourceSubForm'
},
@ -273,9 +295,89 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: "<p>" + i18n._("Override variables found in azure_rm.ini and used by the inventory update script. For a detailed description of these variables ") +
"<a href=\"https://github.com/ansible-collections/community.general/blob/main/scripts/inventory/azure_rm.ini\" target=\"_blank\">" +
i18n._("view azure_rm.ini in the Ansible community.general github repo.") + "</a></p>" +
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/azure/azcollection/azure_rm_inventory.html\" target=\"blank\">azure_rm</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
i18n._("YAML:") + "<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n" +
"<p>" + i18n._("View JSON examples at ") + '<a href="http://www.json.org" target="_blank">www.json.org</a></p>' +
"<p>" + i18n._("View YAML examples at ") + '<a href="http://docs.ansible.com/YAMLSyntax.html" target="_blank">docs.ansible.com</a></p>',
dataContainer: 'body',
subForm: 'sourceSubForm'
},
gce_variables: {
id: 'gce_variables',
label: i18n._('Source Variables'), //"{{vars_label}}" ,
ngShow: "source && source.value == 'gce'",
type: 'textarea',
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
rows: 6,
'default': '---',
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/google/cloud/gcp_compute_inventory.html\" target=\"blank\">gcp_compute</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
i18n._("YAML:") + "<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n" +
"<p>" + i18n._("View JSON examples at ") + '<a href="http://www.json.org" target="_blank">www.json.org</a></p>' +
"<p>" + i18n._("View YAML examples at ") + '<a href="http://docs.ansible.com/YAMLSyntax.html" target="_blank">docs.ansible.com</a></p>',
dataContainer: 'body',
subForm: 'sourceSubForm'
},
tower_variables: {
id: 'tower_variables',
label: i18n._('Source Variables'), //"{{vars_label}}" ,
ngShow: "source && source.value == 'tower'",
type: 'textarea',
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
rows: 6,
'default': '---',
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/awx/awx/tower_inventory.html\" target=\"blank\">tower</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
i18n._("YAML:") + "<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n" +
"<p>" + i18n._("View JSON examples at ") + '<a href="http://www.json.org" target="_blank">www.json.org</a></p>' +
"<p>" + i18n._("View YAML examples at ") + '<a href="http://docs.ansible.com/YAMLSyntax.html" target="_blank">docs.ansible.com</a></p>',
dataContainer: 'body',
subForm: 'sourceSubForm'
},
rhv_variables: {
id: 'rhv_variables',
label: i18n._('Source Variables'), //"{{vars_label}}" ,
ngShow: "source && source.value == 'rhv'",
type: 'textarea',
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
rows: 6,
'default': '---',
parseTypeName: 'envParseType',
dataTitle: i18n._("Source Variables"),
dataPlacement: 'right',
awPopOver: "<p>" + i18n._("Enter variables to configure the inventory source. For a detailed description of how to configure this plugin, see ") +
"<a href=\"http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins\" target=\"blank\">" +
i18n._("Inventory Plugins") + "</a> " + i18n._("in the documentation and the ") +
"<a href=\"https://docs.ansible.com/ansible/latest/collections/ovirt/ovirt/ovirt_inventory.html\" target=\"blank\">ovirt</a> " +
i18n._("plugin configuration guide.") + "</p>" +
"<p>" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "</p>" +
i18n._("JSON:") + "<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +

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,30 +62,30 @@ 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;
}
};
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;
}
}).catch(() => {
@ -100,28 +100,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.manifest = btoa(raw.result);
};
try {
raw.readAsText(event.target.files[0]);
raw.readAsBinaryString(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 valid Red Hat Subscription Manifest.')});
}
};
// 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 +126,9 @@ export default
};
$scope.replacePassword = () => {
if ($scope.user_is_superuser && !$scope.newLicense.file) {
if ($scope.user_is_superuser && !$scope.newLicense.manifest) {
$scope.showPlaceholderPassword = false;
$scope.rhCreds.password = "";
$scope.subscriptionCreds.password = "";
$timeout(() => {
$('.tooltip').remove();
$('#rh-password').focus();
@ -142,9 +137,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 +167,30 @@ 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.manifest) {
payload.manifest = $scope.newLicense.manifest;
} else if ($scope.selectedLicense.fullLicense) {
payload = $scope.selectedLicense.fullLicense;
payload.pool_id = $scope.selectedLicense.fullLicense.pool_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 +213,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>Compliant</translate></span>
<span class="License-redText" ng-show='compliant !== undefined && !compliant'><i class="fa fa-circle License-redText"></i><translate>Out of Compliance</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">
@ -64,53 +58,66 @@
{{license.license_info.current_instances}}
</div>
</div>
<div class="License-field License-greenText" ng-show='license.license_info.available_instances < 9999999'>
<div class="License-field License-greenText" ng-show='license.license_info.available_instances < 9999999 && compliant'>
<div class="License-field--label" translate>Hosts Remaining</div>
<div class="License-field--content">
{{license.license_info.free_instances}}
</div>
</div>
<div class="License-field License-redText" ng-show='license.license_info.free_instances < 1 && !compliant'>
<div class="License-field--label" translate>Hosts Remaining</div>
<div class="License-field--content">
{{license.license_info.free_instances}}
</div>
</div>
</div>
<div class="License-upgradeText" translate>If you are ready to upgrade, please contact us by clicking the button below</div>
<a href="https://www.ansible.com/renew" target="_blank"><button class="btn btn-primary" translate>Upgrade</button></a>
<div class="License-upgradeText" translate>If you are ready to upgrade or renew, please contact us by clicking the button below.</div>
<a href="https://www.redhat.com/contact" target="_blank"><button class="btn btn-primary" translate>Contact Us</button></a>
</div>
</div>
<div class="License-management" ng-class="{'License-management--missingLicense' : licenseMissing}">
<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 Red Hat Ansible Automation Platform! Please complete the steps below to activate your subscription.</div>
<div class="AddPermissions-directions" ng-if="licenseMissing">
<span class="AddPermissions-directionNumber">
1
</span>
<span class="License-helperText">
<translate>If you do not have a subscription, you can visit Red Hat to obtain a trial subscription.</translate>
</span>
</div>
<button class="License-downloadLicenseButton btn btn-primary" ng-if="licenseMissing" ng-click="downloadLicense()">
<translate>Request Subscription</translate>
</button>
<div class="License-file-container">
<div class="AddPermissions-directions" ng-if="licenseMissing">
<span class="AddPermissions-directionNumber" ng-if="licenseMissing">
2
</span>
<span class="License-helperText">
<translate>Select your Ansible Automation Platform subscription to use.</translate>
</span>
</div>
</div>
<div class="input-group License-file--container">
<div class="License-file--left">
<div class="d-block w-100">
<div class="AddPermissions-directions" ng-if="licenseMissing">
<span class="AddPermissions-directionNumber">
1
</span>
<span class="License-helperText">
<translate>Please click the button below to visit Ansible's website to get a Tower license key.</translate>
</span>
</div>
<button class="License-downloadLicenseButton btn btn-primary" ng-if="licenseMissing" ng-click="downloadLicense()">
<translate>Request License</translate>
</button>
<div class="AddPermissions-directions">
<span class="AddPermissions-directionNumber" ng-if="licenseMissing">
2
</span>
<span class="License-helperText">
<translate>Choose your license file, agree to the End User License Agreement, and click submit.</translate>
<translate>Upload a Red Hat Subscription Manifest containing your subscription. To generate your subscription manifest, go to <a href="https://access.redhat.com/management/subscription_allocations" target="_blank">subscription allocations</a> on the Red Hat Customer Portal.</translate>
</span>
</div>
<div class="License-subTitleText">
<span class="Form-requiredAsterisk">*</span>
<translate>License</translate>
<translate>Red Hat Subscription Manifest</translate>
<a aria-label="{{'Show help text' | translate}}" id="subscription-manifest-popover" href="" aw-pop-over="A subscription manifest is an export of a Red Hat Subscription. To generate a subscription manifest, go to <a href=https://access.redhat.com/management/subscription_allocations target=_blank>access.redhat.com</a>.<br>For more information, see the <a href=https://docs.ansible.com/ansible-tower/latest/html/userguide/import_license.html>User Guide</a>.</translate>" data-placement="top" data-container="body" over-title="Subscription Manifest" class="help-link">
<i class="fa fa-question-circle"></i>
</a>
</div>
<div class="License-helperText License-licenseStepHelp" translate>Upload a license file</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 +132,12 @@
<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>
<translate>Provide your Red Hat or Red Hat Satellite credentials below and you can choose from a list of your available subscriptions. 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 +150,11 @@
</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-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 SUBSCRIPTIONS</button>
</div>
<div ng-if="selectedLicense.fullLicense">
<div class="at-RowItem-label" translate>
@ -158,6 +165,14 @@
</div>
</div>
</div>
<div class="License-helperText">
<span class="AddPermissions-directionNumber" ng-if="licenseMissing">
3
</span>
<span class="License-helperText">
<translate>Agree to the End User License Agreement, and click submit.</translate>
</span>
</div>
<div class="License-subTitleText">
<span class="Form-requiredAsterisk">*</span>
<translate>End User License Agreement</translate>
@ -200,7 +215,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.manifest && !selectedLicense.fullLicense) || newLicense.eula == null || !user_is_superuser" translate>Submit</button>
</div>
</div>
</div>
@ -223,12 +238,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 +275,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,20 @@ 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;
}
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} );
}
};
}

View File

@ -7,7 +7,7 @@ describe('Controller: LicenseController', () => {
ConfigService,
ProcessErrors,
config,
rhCreds;
subscriptionCreds;
beforeEach(angular.mock.module('awApp'));
beforeEach(angular.mock.module('license', ($provide) => {
@ -23,7 +23,7 @@ describe('Controller: LicenseController', () => {
version: '3.1.0-devel'
};
rhCreds = {
subscriptionCreds = {
password: '$encrypted$',
username: 'foo',
}
@ -33,21 +33,21 @@ describe('Controller: LicenseController', () => {
$provide.value('ConfigService', ConfigService);
$provide.value('ProcessErrors', ProcessErrors);
$provide.value('config', config);
$provide.value('rhCreds', rhCreds);
$provide.value('subscriptionCreds', subscriptionCreds);
}));
beforeEach(angular.mock.inject( ($rootScope, $controller, _ConfigService_, _ProcessErrors_, _config_, _rhCreds_) => {
beforeEach(angular.mock.inject( ($rootScope, $controller, _ConfigService_, _ProcessErrors_, _config_, _subscriptionCreds_) => {
scope = $rootScope.$new();
ConfigService = _ConfigService_;
ProcessErrors = _ProcessErrors_;
config = _config_;
rhCreds = _rhCreds_;
subscriptionCreds = _subscriptionCreds_;
LicenseController = $controller('licenseController', {
$scope: scope,
ConfigService: ConfigService,
ProcessErrors: ProcessErrors,
config: config,
rhCreds: rhCreds
subscriptionCreds: subscriptionCreds
});
}));

View File

@ -111,7 +111,7 @@ def main():
# Attempt to look up an existing item based on the provided data
existing_item = module.get_one('instance_groups', name_or_id=name)
if state is 'absent':
if state == 'absent':
# If the state was absent we can let the module delete it if needed, the module will handle exiting from this
module.delete_if_needed(existing_item)

View File

@ -21,11 +21,11 @@ description:
- Get or Set Ansible Tower license. See
U(https://www.ansible.com/tower) for an overview.
options:
data:
manifest:
description:
- The contents of the license file
- file path to a Red Hat subscription manifest (a .zip file)
required: True
type: dict
type: str
eula_accepted:
description:
- Whether or not the EULA is accepted.
@ -39,10 +39,11 @@ RETURN = ''' # '''
EXAMPLES = '''
- name: Set the license using a file
license:
data: "{{ lookup('file', '/tmp/my_tower.license') }}"
manifest: "/tmp/my_manifest.zip"
eula_accepted: True
'''
import base64
from ..module_utils.tower_api import TowerAPIModule
@ -50,29 +51,31 @@ def main():
module = TowerAPIModule(
argument_spec=dict(
data=dict(type='dict', required=True),
manifest=dict(type='str', required=True),
eula_accepted=dict(type='bool', required=True),
),
)
json_output = {'changed': False}
json_output = {'changed': True}
if not module.params.get('eula_accepted'):
module.fail_json(msg='You must accept the EULA by passing in the param eula_accepted as True')
json_output['old_license'] = module.get_endpoint('settings/system/')['json']['LICENSE']
new_license = module.params.get('data')
try:
manifest = base64.b64encode(
open(module.params.get('manifest'), 'rb').read()
)
except OSError as e:
module.fail_json(msg=str(e))
if json_output['old_license'] != new_license:
json_output['changed'] = True
# Deal with check mode
if module.check_mode:
module.exit_json(**json_output)
# Deal with check mode
if module.check_mode:
module.exit_json(**json_output)
# We need to add in the EULA
new_license['eula_accepted'] = True
module.post_endpoint('config', data=new_license)
module.post_endpoint('config', data={
'eula_accepted': True,
'manifest': manifest.decode()
})
module.exit_json(**json_output)

View File

@ -34,6 +34,19 @@
regexp: "^ NAME = 'awx.awx.tower' # REPLACE$"
replace: " NAME = '{{ collection_namespace }}.{{ collection_package }}.tower' # REPLACE"
- name: get list of test files
find:
paths: "{{ collection_path }}/tests/integration/targets/"
recurse: true
register: test_files
- name: Change lookup plugin fqcn usage in tests
replace:
path: "{{ item.path }}"
regexp: 'awx.awx'
replace: '{{ collection_namespace }}.{{ collection_package }}'
loop: "{{ test_files.files }}"
- name: Get sanity tests to work with non-default name
lineinfile:
path: "{{ collection_path }}/tests/sanity/ignore-2.10.txt"

View File

@ -31,16 +31,38 @@ EXPORTABLE_RELATIONS = [
'NotificationTemplates',
'WorkflowJobTemplateNodes',
'Credentials',
'Hosts',
'Groups',
]
EXPORTABLE_DEPENDENT_OBJECTS = [
'Labels',
'SurveySpec',
'Schedules',
# WFJT Nodes are a special case, we want full data for the create
# view and natural keys for the attach views.
'WorkflowJobTemplateNodes',
# These are special-case related objects, where we want only in this
# case to export a full object instead of a natural key reference.
DEPENDENT_EXPORT = [
('JobTemplate', 'labels'),
('JobTemplate', 'survey_spec'),
('JobTemplate', 'schedules'),
('WorkflowJobTemplate', 'labels'),
('WorkflowJobTemplate', 'survey_spec'),
('WorkflowJobTemplate', 'schedules'),
('WorkflowJobTemplate', 'workflow_nodes'),
('Project', 'schedules'),
('InventorySource', 'schedules'),
('Inventory', 'groups'),
('Inventory', 'hosts'),
]
# This is for related views where it is unneeded to export anything,
# such as because it is a calculated subset of objects covered by a
# different view.
DEPENDENT_NONEXPORT = [
('InventorySource', 'groups'),
('InventorySource', 'hosts'),
('Inventory', 'root_groups'),
('Group', 'all_hosts'),
('Group', 'potential_children'),
('Host', 'all_groups'),
]
@ -61,6 +83,9 @@ class ApiV2(base.Base):
if _page.json.get('managed_by_tower'):
log.debug("%s is managed by Tower, skipping.", _page.endpoint)
return None
# Drop any hosts, groups, or inventories that were pulled in programmatically by an inventory source.
if _page.json.get('has_inventory_sources'):
return None
if post_fields is None: # Deprecated endpoint or insufficient permissions
log.error("Object export failed: %s", _page.endpoint)
return None
@ -107,8 +132,9 @@ class ApiV2(base.Base):
rel = rel_endpoint._create()
is_relation = rel.__class__.__name__ in EXPORTABLE_RELATIONS
is_dependent = rel.__class__.__name__ in EXPORTABLE_DEPENDENT_OBJECTS
if not (is_relation or is_dependent):
is_dependent = (_page.__item_class__.__name__, key) in DEPENDENT_EXPORT
is_blocked = (_page.__item_class__.__name__, key) in DEPENDENT_NONEXPORT
if is_blocked or not (is_relation or is_dependent):
continue
rel_post_fields = utils.get_post_fields(rel_endpoint, self._cache)
@ -117,10 +143,10 @@ class ApiV2(base.Base):
continue
is_attach = 'id' in rel_post_fields # This is not a create-only endpoint.
if is_relation and is_attach:
by_natural_key = True
elif is_dependent:
if is_dependent:
by_natural_key = False
elif is_relation and is_attach and not is_blocked:
by_natural_key = True
else:
continue

View File

@ -11,15 +11,9 @@ class Config(base.Base):
'ami-id' in self.license_info or \
'instance-id' in self.license_info
@property
def is_demo_license(self):
return self.license_info.get('demo', False) or \
self.license_info.get('key_present', False)
@property
def is_valid_license(self):
return self.license_info.get('valid_key', False) and \
'license_key' in self.license_info and \
'instance_count' in self.license_info
@property
@ -31,16 +25,6 @@ class Config(base.Base):
def is_awx_license(self):
return self.license_info.get('license_type', None) == 'open'
@property
def is_legacy_license(self):
return self.is_valid_license and \
self.license_info.get('license_type', None) == 'legacy'
@property
def is_basic_license(self):
return self.is_valid_license and \
self.license_info.get('license_type', None) == 'basic'
@property
def is_enterprise_license(self):
return self.is_valid_license and \
@ -52,4 +36,11 @@ class Config(base.Base):
return [k for k, v in self.license_info.get('features', {}).items() if v]
class ConfigAttach(page.Page):
def attach(self, **kwargs):
return self.post(json=kwargs).json
page.register_page(resources.config, Config)
page.register_page(resources.config_attach, ConfigAttach)

View File

@ -259,6 +259,7 @@ class Group(HasCreate, HasVariables, base.Base):
dependencies = [Inventory]
optional_dependencies = [Credential, InventoryScript]
NATURAL_KEY = ('name', 'inventory')
@property
def is_root_group(self):
@ -367,6 +368,7 @@ page.register_page([resources.groups,
class Host(HasCreate, HasVariables, base.Base):
dependencies = [Inventory]
NATURAL_KEY = ('name', 'inventory')
def payload(self, inventory, **kwargs):
payload = PseudoNamespace(

View File

@ -16,6 +16,7 @@ class Resources(object):
_auth = 'auth/'
_authtoken = 'authtoken/'
_config = 'config/'
_config_attach = 'config/attach/'
_credential = r'credentials/\d+/'
_credential_access_list = r'credentials/\d+/access_list/'
_credential_copy = r'credentials/\d+/copy/'

View File

@ -4,6 +4,7 @@ set -ue
requirements_in="$(readlink -f ./requirements.in)"
requirements_ansible_in="$(readlink -f ./requirements_ansible.in)"
requirements="$(readlink -f ./requirements.txt)"
requirements_git="$(readlink -f ./requirements_git.txt)"
requirements_ansible="$(readlink -f ./requirements_ansible.txt)"
pip_compile="pip-compile --no-header --quiet -r --allow-unsafe"
@ -31,7 +32,11 @@ generate_requirements_v3() {
install_deps
${pip_compile} --output-file requirements.txt "${requirements_in}"
${pip_compile} --output-file requirements.txt "${requirements_in}" "${requirements_git}"
# consider the git requirements for purposes of resolving deps
# Then remove any git+ lines from requirements.txt
cp requirements.txt requirements_tmp.txt
grep -v "^git+" requirements_tmp.txt > requirements.txt && rm requirements_tmp.txt
${pip_compile} --output-file requirements_ansible_py3.txt "${requirements_ansible_in}"
}