mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 18:09:57 -03:30
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:
commit
17b5b531bf
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||
|
||||
4
Makefile
4
Makefile
@ -650,9 +650,11 @@ awx/projects:
|
||||
docker-compose-isolated: awx/projects
|
||||
CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml -f tools/docker-isolated-override.yml up
|
||||
|
||||
COMPOSE_UP_OPTS ?=
|
||||
|
||||
# Docker Compose Development environment
|
||||
docker-compose: docker-auth awx/projects
|
||||
CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml up --no-recreate awx
|
||||
CURRENT_UID=$(shell id -u) OS="$(shell docker info | grep 'Operating System')" TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose.yml $(COMPOSE_UP_OPTS) up --no-recreate awx
|
||||
|
||||
docker-compose-cluster: docker-auth awx/projects
|
||||
CURRENT_UID=$(shell id -u) TAG=$(COMPOSE_TAG) DEV_DOCKER_TAG_BASE=$(DEV_DOCKER_TAG_BASE) docker-compose -f tools/docker-compose-cluster.yml up
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,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:
|
||||
|
||||
@ -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()
|
||||
|
||||
26
awx/conf/migrations/0008_subscriptions.py
Normal file
26
awx/conf/migrations/0008_subscriptions.py
Normal 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)
|
||||
]
|
||||
34
awx/conf/migrations/_subscriptions.py
Normal file
34
awx/conf/migrations/_subscriptions.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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',
|
||||
)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
@ -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'
|
||||
|
||||
@ -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,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
390
awx/main/utils/licensing.py
Normal 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
|
||||
@ -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 {};
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 /> \"somevar\": \"somevalue\",<br /> \"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 /> \"somevar\": \"somevalue\",<br /> \"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 /> \"somevar\": \"somevalue\",<br /> \"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 /> \"somevar\": \"somevalue\",<br /> \"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 /> \"somevar\": \"somevalue\",<br /> \"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 /> \"somevar\": \"somevalue\",<br /> \"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 /> \"somevar\": \"somevalue\",<br /> \"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 /> \"somevar\": \"somevalue\",<br /> \"password\": \"magic\"<br /> }</blockquote>\n" +
|
||||
|
||||
@ -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,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
|
||||
|
||||
@ -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 > 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
|
||||
|
||||
@ -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 {};
|
||||
});
|
||||
|
||||
@ -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} );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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/'
|
||||
|
||||
@ -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}"
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user