diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index ede960ecb6..ab7d61fd23 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -14,6 +14,7 @@ from awx.api.views import ( ApiV2RootView, ApiV2PingView, ApiV2ConfigView, + ApiV2SubscriptionView, AuthView, UserMeList, DashboardView, @@ -94,6 +95,7 @@ v2_urls = [ url(r'^metrics/$', MetricsView.as_view(), name='metrics_view'), 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'^auth/$', AuthView.as_view()), url(r'^me/$', UserMeList.as_view(), name='user_me_list'), url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index a6d92e9578..9302249e67 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -147,6 +147,7 @@ from awx.api.views.root import ( # noqa ApiV2RootView, ApiV2PingView, ApiV2ConfigView, + ApiV2SubscriptionView, ) diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 922041c8b5..91f6b62149 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -17,6 +17,8 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework import status +import requests + from awx.api.generics import APIView from awx.main.ha import is_ha_environment from awx.main.utils import ( @@ -169,6 +171,45 @@ class ApiV2PingView(APIView): return Response(response) +class ApiV2SubscriptionView(APIView): + + permission_classes = (IsAuthenticated,) + name = _('Configuration') + swagger_topic = 'System Configuration' + + def check_permissions(self, request): + super(ApiV2SubscriptionView, 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): + from awx.main.utils.common import get_licenser + data = request.data.copy() + if data.get('rh_password') == '$encrypted$': + data['rh_password'] = settings.REDHAT_PASSWORD + try: + user, pw = data.get('rh_username'), data.get('rh_password') + validated = get_licenser().validate_rh(user, pw) + if user: + settings.REDHAT_USERNAME = data['rh_username'] + if pw: + settings.REDHAT_PASSWORD = data['rh_password'] + except Exception as exc: + msg = _("Invalid License") + if ( + isinstance(exc, requests.exceptions.HTTPError) and + getattr(getattr(exc, 'response', None), 'status_code', None) == 401 + ): + msg = _("The provided credentials are invalid (HTTP 401).") + if isinstance(exc, (ValueError, OSError)) and exc.args: + msg = exc.args[0] + logger.exception(smart_text(u"Invalid license submitted."), + extra=dict(actor=request.user.username)) + return Response({"error": msg}, status=status.HTTP_400_BAD_REQUEST) + + return Response(validated) + + class ApiV2ConfigView(APIView): permission_classes = (IsAuthenticated,) diff --git a/awx/main/access.py b/awx/main/access.py index 1a87eff38a..e9957656de 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -317,10 +317,19 @@ class BaseAccess(object): validation_info['time_remaining'] = 99999999 validation_info['grace_period_remaining'] = 99999999 + report_violation = lambda message: logger.error(message) + + if ( + validation_info.get('trial', False) is True or + validation_info['instance_count'] == 10 # basic 10 license + ): + def report_violation(message): + raise PermissionDenied(message) + if check_expiration and validation_info.get('time_remaining', None) is None: raise PermissionDenied(_("License is missing.")) - if check_expiration and validation_info.get("grace_period_remaining") <= 0: - raise PermissionDenied(_("License has expired.")) + elif check_expiration and validation_info.get("grace_period_remaining") <= 0: + report_violation(_("License has expired.")) free_instances = validation_info.get('free_instances', 0) available_instances = validation_info.get('available_instances', 0) @@ -328,11 +337,11 @@ class BaseAccess(object): if add_host_name: host_exists = Host.objects.filter(name=add_host_name).exists() if not host_exists and free_instances == 0: - raise PermissionDenied(_("License count of %s instances has been reached.") % available_instances) + report_violation(_("License count of %s instances has been reached.") % available_instances) elif not host_exists and free_instances < 0: - raise PermissionDenied(_("License count of %s instances has been exceeded.") % available_instances) + report_violation(_("License count of %s instances has been exceeded.") % available_instances) elif not add_host_name and free_instances < 0: - raise PermissionDenied(_("Host count exceeds available instances.")) + report_violation(_("Host count exceeds available instances.")) def check_org_host_limit(self, data, add_host_name=None): validation_info = get_licenser().validate() diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 6ae7ba69d5..7a23c1a63f 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -919,7 +919,8 @@ class Command(BaseCommand): new_count = Host.objects.active_count() if time_remaining <= 0 and not license_info.get('demo', False): logger.error(LICENSE_EXPIRED_MESSAGE) - raise CommandError("License has expired!") + if license_info.get('trial', False) is True: + raise CommandError("License has expired!") # special check for tower-type inventory sources # but only if running the plugin TOWER_SOURCE_FILES = ['tower.yml', 'tower.yaml'] @@ -936,7 +937,11 @@ class Command(BaseCommand): logger.error(DEMO_LICENSE_MESSAGE % d) else: logger.error(LICENSE_MESSAGE % d) - raise CommandError('License count exceeded!') + if ( + license_info.get('trial', False) is True or + license_info['instance_count'] == 10 # basic 10 license + ): + raise CommandError('License count exceeded!') def check_org_host_limit(self): license_info = get_licenser().validate() diff --git a/awx/ui/client/legacy/styles/ansible-ui.less b/awx/ui/client/legacy/styles/ansible-ui.less index 708cc44496..a85621fd9b 100644 --- a/awx/ui/client/legacy/styles/ansible-ui.less +++ b/awx/ui/client/legacy/styles/ansible-ui.less @@ -1595,7 +1595,7 @@ tr td button i { padding: 20px 0; .alert { - padding: 10px; + padding: 0px; margin: 0; word-wrap: break-word; } diff --git a/awx/ui/client/src/configuration/forms/settings-form.route.js b/awx/ui/client/src/configuration/forms/settings-form.route.js index a03b4cbfa5..20ca06fd7f 100644 --- a/awx/ui/client/src/configuration/forms/settings-form.route.js +++ b/awx/ui/client/src/configuration/forms/settings-form.route.js @@ -53,4 +53,24 @@ export default { } }); }], + resolve: { + rhCreds: ['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; + } + + if (data.REDHAT_PASSWORD && data.REDHAT_PASSWORD !== "") { + rhCreds.REDHAT_PASSWORD = data.REDHAT_PASSWORD; + } + + return rhCreds; + }).catch(() => { + return {}; + }); + }] + } }; \ No newline at end of file diff --git a/awx/ui/client/src/license/checkLicense.factory.js b/awx/ui/client/src/license/checkLicense.factory.js index 04a658fa82..e76f1ea0b7 100644 --- a/awx/ui/client/src/license/checkLicense.factory.js +++ b/awx/ui/client/src/license/checkLicense.factory.js @@ -5,29 +5,29 @@ *************************************************/ export default - ['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', - 'ConfigService', - function($state, $rootScope, Rest, GetBasePath, ProcessErrors, - ConfigService){ + ['$state', '$rootScope', 'Rest', 'GetBasePath', + 'ConfigService', '$q', + function($state, $rootScope, Rest, GetBasePath, + ConfigService, $q){ return { get: function() { var config = ConfigService.get(); return config.license_info; }, - post: function(license, eula){ + post: function(payload, eula){ var defaultUrl = GetBasePath('config'); Rest.setUrl(defaultUrl); - var data = license; + var data = payload; data.eula_accepted = eula; + return Rest.post(JSON.stringify(data)) .then((response) =>{ return response.data; }) - .catch(({res, status}) => { - ProcessErrors($rootScope, res, status, null, {hdr: 'Error!', - msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); - }); + .catch(({data}) => { + return $q.reject(data); + }); }, valid: function(license) { diff --git a/awx/ui/client/src/license/license.block.less b/awx/ui/client/src/license/license.block.less index 08a6c624a6..f86d323ab8 100644 --- a/awx/ui/client/src/license/license.block.less +++ b/awx/ui/client/src/license/license.block.less @@ -26,6 +26,21 @@ display: block; width: 100%; } +.License-file--left { + display: flex; + flex:1; + overflow: hidden; +} +.License-file--middle { + display: flex; + flex: 0 0 auto; + padding: 0px 20px; + flex-direction: column; +} +.License-file--right { + display: flex; + flex:1; +} .License-submit--success.ng-hide-add, .License-submit--success.ng-hide-remove { transition: all ease-in-out 0.5s; } @@ -109,10 +124,11 @@ } } -.License-submit--success{ +.License-submit--success, .License-submit--failure { margin: 0; } .License-file--container { + display: flex; input[type=file] { display: none; } @@ -148,3 +164,127 @@ padding: 10px 0; font-weight: bold; } + +.License-separator { + display: flex; + flex: 1; + background: linear-gradient(#d7d7d7, #d7d7d7) no-repeat center/2px 100%; +} + +.License-licenseStepHelp { + font-size: 12px; + font-style: italic; + margin-bottom: 10px; +} + +.License-filePicker { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.License-rhCredField { + margin-bottom: 10px; +} + +.License-label { + color: @field-label; + font-weight: 400; +} + +.License-action { + display: flex; + flex-direction: row; + align-content:flex-end; +} + +.License-actionError { + flex: 1; +} + +.License-subSelectorModal { + height: 100%; + width: 100%; + position: fixed; + top: 0; + left: 0; + background: rgba(0, 0, 0, 0.3); + z-index: 1040; + display: flex; + align-items: center; + justify-content: center; +} + +.License-modal { + width: 750px; +} + +.License-modalBody { + border: 1px solid @b7grey; + max-height: 550px; + overflow: scroll; + border-radius: 4px; +} + +.License-modalRow { + display: flex; + padding: 10px; +} + +.License-modalRow:not(:last-of-type) { + border-bottom: 1px solid @b7grey; +} + +.License-modalRowRadio { + flex: 0 0 40px; + display: flex; + align-items: center; +} + +.License-trialTag { + font-weight: 100; + background-color: #ebebeb; + border-radius: 5px; + color: #606060; + font-size: 10px; + margin-right: 10px; + padding: 3px 9px; + line-height: 14px; + word-break: keep-all; + display: inline-flex; +} + +.License-introText { + margin-bottom: 10px; +} + +.License-getLicensesButton { + display: flex; + justify-content: flex-end; + margin-bottom: 20px; +} + +.License-checkboxLabel { + margin-left: 5px; + font-weight: normal; +} + +.License-modalRowDetails { + flex: 1; +} + +.License-modalRowDetailsLabel { + font-weight: normal; + width: 100%; +} + +.License-modalRowDetailsRow { + margin-bottom: 10px; +} + +.License-modalRowDetails--50 { + display: flex; + flex-basis: 50%; + align-items: center; + line-height: 21px; +} diff --git a/awx/ui/client/src/license/license.controller.js b/awx/ui/client/src/license/license.controller.js index ac80686a4f..c0f9012edf 100644 --- a/awx/ui/client/src/license/license.controller.js +++ b/awx/ui/client/src/license/license.controller.js @@ -7,11 +7,10 @@ import {N_} from "../i18n"; export default - ['Wait', '$state', '$scope', '$rootScope', - 'ProcessErrors', 'CheckLicense', 'moment','$window', - 'ConfigService', 'pendoService', 'insightsEnablementService', 'i18n', 'config', - function(Wait, $state, $scope, $rootScope, ProcessErrors, CheckLicense, moment, - $window, ConfigService, pendoService, insightsEnablementService, i18n, config) { + ['Wait', '$state', '$scope', '$rootScope', 'ProcessErrors', 'CheckLicense', 'moment', '$timeout', 'Rest', + '$window', 'ConfigService', 'pendoService', 'insightsEnablementService', 'i18n', 'config', 'rhCreds', 'GetBasePath', + function(Wait, $state, $scope, $rootScope, ProcessErrors, CheckLicense, moment, $timeout, Rest, + $window, ConfigService, pendoService, insightsEnablementService, i18n, config, rhCreds, GetBasePath) { const calcDaysRemaining = function(seconds) { // calculate the number of days remaining on the license @@ -33,10 +32,12 @@ export default }; const reset = function() { - document.getElementById('License-form').reset(); + $scope.newLicense.eula = undefined; + $scope.rhCreds = {}; + $scope.selectedLicense = {}; }; - const init = function(config) { + const initVars = (config) => { // license/license.partial.html compares fileName $scope.fileName = N_("No file selected."); @@ -53,13 +54,44 @@ export default $scope.time.expiresOn = calcExpiresOn($scope.license.license_info.license_date); $scope.valid = CheckLicense.valid($scope.license.license_info); $scope.compliant = $scope.license.license_info.compliant; + $scope.selectedLicense = {}; $scope.newLicense = { pendo: true, insights: true }; + + $scope.rhCreds = {}; + + if (rhCreds.REDHAT_USERNAME && rhCreds.REDHAT_USERNAME !== "") { + $scope.rhCreds.username = rhCreds.REDHAT_USERNAME; + } + + if (rhCreds.REDHAT_PASSWORD && rhCreds.REDHAT_PASSWORD !== "") { + $scope.rhCreds.password = rhCreds.REDHAT_PASSWORD; + $scope.showPlaceholderPassword = true; + } }; - init(config); + const updateRHCreds = (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.REDHAT_PASSWORD && data.REDHAT_PASSWORD !== "") { + $scope.rhCreds.password = data.REDHAT_PASSWORD; + $scope.showPlaceholderPassword = true; + } + }).catch(() => { + initVars(config); + }); + }; + + initVars(config); $scope.getKey = function(event) { // Mimic HTML5 spec, show filename @@ -87,7 +119,7 @@ export default // 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) { + if($scope.user_is_superuser && (!$scope.rhCreds.username || $scope.rhCreds.username === '') && (!$scope.rhCreds.password || $scope.rhCreds.password === '')) { $('#License-file').click(); } }; @@ -96,44 +128,112 @@ export default $window.open('https://www.ansible.com/license', '_blank'); }; - $scope.submit = function() { - Wait('start'); - CheckLicense.post($scope.newLicense.file, $scope.newLicense.eula) - .then((licenseInfo) => { - reset(); + $scope.replacePassword = () => { + if ($scope.user_is_superuser && !$scope.newLicense.file) { + $scope.showPlaceholderPassword = false; + $scope.rhCreds.password = ""; + $timeout(() => { + $('.tooltip').remove(); + $('#rh-password').focus(); + }); + } + }; - ConfigService.delete(); - ConfigService.getConfig(licenseInfo) - .then(function(config) { - - if ($rootScope.licenseMissing === true) { - if ($scope.newLicense.pendo) { - pendoService.updatePendoTrackingState('detailed'); - pendoService.issuePendoIdentity(); - } else { - pendoService.updatePendoTrackingState('off'); - } - - if ($scope.newLicense.insights) { - insightsEnablementService.updateInsightsTrackingState(true); - } else { - insightsEnablementService.updateInsightsTrackingState(false); - } - - $state.go('dashboard', { - licenseMissing: false - }); - } else { - init(config); - $scope.success = true; - $rootScope.licenseMissing = false; - // for animation purposes - const successTimeout = setTimeout(function() { - $scope.success = false; - clearTimeout(successTimeout); - }, 4000); - } - }); + $scope.lookupLicenses = () => { + if ($scope.rhCreds.username && $scope.rhCreds.password) { + Wait('start'); + ConfigService.getSubscriptions($scope.rhCreds.username, $scope.rhCreds.password) + .then(({data}) => { + Wait('stop'); + if (data && data.length > 0) { + $scope.rhLicenses = data; + if ($scope.selectedLicense.fullLicense) { + $scope.selectedLicense.modalKey = $scope.selectedLicense.fullLicense.license_key; + } + $scope.showLicenseModal = true; + } else { + ProcessErrors($scope, data, status, null, { + hdr: i18n._('No Licenses Found'), + msg: i18n._('We were unable to locate licenses associated with this account') + }); + } + }) + .catch(({data, status}) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error Fetching Licenses') }); - }; + }); + } + }; + + $scope.confirmLicenseSelection = () => { + $scope.showLicenseModal = false; + $scope.selectedLicense.fullLicense = $scope.rhLicenses.find((license) => { + return license.license_key === $scope.selectedLicense.modalKey; + }); + $scope.selectedLicense.modalKey = undefined; + }; + + $scope.cancelLicenseLookup = () => { + $scope.showLicenseModal = false; + $scope.selectedLicense.modalKey = undefined; + }; + + $scope.submit = function() { + Wait('start'); + let payload = {}; + if ($scope.newLicense.file) { + payload = $scope.newLicense.file; + } else if ($scope.selectedLicense.fullLicense) { + payload = $scope.selectedLicense.fullLicense; + if ($scope.rhCreds.username && $scope.rhCreds.password) { + payload.rh_password = $scope.rhCreds.password; + payload.rh_username = $scope.rhCreds.username; + } + } + + CheckLicense.post(payload, $scope.newLicense.eula) + .then((licenseInfo) => { + reset(); + + ConfigService.delete(); + ConfigService.getConfig(licenseInfo) + .then(function(config) { + + if ($rootScope.licenseMissing === true) { + if ($scope.newLicense.pendo) { + pendoService.updatePendoTrackingState('detailed'); + pendoService.issuePendoIdentity(); + } else { + pendoService.updatePendoTrackingState('off'); + } + + if ($scope.newLicense.insights) { + insightsEnablementService.updateInsightsTrackingState(true); + } else { + insightsEnablementService.updateInsightsTrackingState(false); + } + + $state.go('dashboard', { + licenseMissing: false + }); + } else { + updateRHCreds(config); + $scope.success = true; + $rootScope.licenseMissing = false; + // for animation purposes + const successTimeout = setTimeout(function() { + $scope.success = false; + clearTimeout(successTimeout); + }, 4000); + } + }); + }).catch(({data, status}) => { + Wait('stop'); + ProcessErrors($scope, data, status, null, { + hdr: i18n._('Error Applying License') + }); + }); + }; }]; diff --git a/awx/ui/client/src/license/license.partial.html b/awx/ui/client/src/license/license.partial.html index 96b44b6bb9..924ec5c459 100644 --- a/awx/ui/client/src/license/license.partial.html +++ b/awx/ui/client/src/license/license.partial.html @@ -8,7 +8,7 @@
License
Valid License - Invalid License + Invalid License
@@ -75,78 +75,193 @@
{{title}}
-
Welcome to Ansible Tower! Please complete the steps below to acquire a license.
-
- - 1 - - - Please click the button below to visit Ansible's website to get a Tower license key. - +
Welcome to Ansible Tower! Please complete the steps below to acquire a license.
+
+
+
+
+ + 1 + + + Please click the button below to visit Ansible's website to get a Tower license key. + +
+ + + +
+ + 2 + + + Choose your license file, agree to the End User License Agreement, and click submit. + +
+
+ * + License +
+
Upload a license file
+
+ Browse + {{fileName|translate}} + +
+
+
+
+
+
OR
+
+
+
+
+
+ + 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. + +
+
+ + +
+
+ +
+ + + + +
+
+ +
+
+
+ GET LICENSES +
+
+
+ Selected +
+ {{selectedLicense.fullLicense.subscription_name}} +
+
+
- - - -
- - 2 - - - Choose your license file, agree to the End User License Agreement, and click submit. - +
+ * + End User License Agreement
- -
-
- * - License File +
{{ license.eula }}
+
+
+
-
- Browse - {{fileName|translate}} - -
-
- * - End User License Agreement -
-
{{ license.eula }}
-
+
+
+ Tracking and Analytics +
+
+ + + By default, Tower collects and transmits analytics data on Tower usage to Red Hat. There are two categories of data collected by Tower. For more information, see this Tower documentation page. Uncheck the following boxes to disable this feature. + +
- + +
+
+
-
- Tracking and Analytics -
-
- - - By default, Tower collects and transmits analytics data on Tower usage to Red Hat. There are two categories of data collected by Tower. For more information, see this Tower documentation page. Uncheck the following boxes to disable this feature. - -
-
- - User analytics: This data is used to enhance future releases of the Tower Software and help streamline customer experience and success. -
-
- - Automation analytics: This data is used to enhance future releases of the Tower Software and to provide Automation Insights to Tower subscribers. -
-
-
-
- +
+
+
Save successful!
- +
+ +
+
+
+ +
diff --git a/awx/ui/client/src/license/license.route.js b/awx/ui/client/src/license/license.route.js index 98ba25d79c..4c1251ca6a 100644 --- a/awx/ui/client/src/license/license.route.js +++ b/awx/ui/client/src/license/license.route.js @@ -17,30 +17,49 @@ export default { ncyBreadcrumb: { label: N_('LICENSE') }, - onEnter: ['$state', 'ConfigService', (state, configService) => { - return configService.getConfig() - .then(config => { - if (_.get(config, 'license_info.license_type') === 'open') { - return state.go('setup'); - } - }); - }], + onEnter: ['$state', 'ConfigService', (state, configService) => { + return configService.getConfig() + .then(config => { + if (_.get(config, 'license_info.license_type') === 'open') { + return state.go('setup'); + } + }); + }], resolve: { features: ['CheckLicense', '$rootScope', function(CheckLicense, $rootScope) { if($rootScope.licenseMissing === undefined){ return CheckLicense.notify(); } - - }], + } + ], config: ['ConfigService', 'CheckLicense', '$rootScope', function(ConfigService, CheckLicense, $rootScope) { ConfigService.delete(); - return ConfigService.getConfig() - .then(function(config){ - $rootScope.licenseMissing = (CheckLicense.valid(config.license_info) === false) ? true : false; - return config; + return ConfigService.getConfig() + .then(function(config){ + $rootScope.licenseMissing = (CheckLicense.valid(config.license_info) === false) ? true : false; + return config; + }); + } + ], + rhCreds: ['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; + } + + if (data.REDHAT_PASSWORD && data.REDHAT_PASSWORD !== "") { + rhCreds.REDHAT_PASSWORD = data.REDHAT_PASSWORD; + } + + return rhCreds; + }).catch(() => { + return {}; }); - }] + }] }, }; diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 8c3a2b9d7b..c731a54bba 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -238,9 +238,9 @@ angular.module('Utilities', ['RestServices', 'Utilities']) msg = ""; _.forOwn(data, function(value, key) { if (Array.isArray(data[key])) { - msg += `${key}: ${data[key][0]}`; + msg += `${key.toUpperCase()}: ${data[key][0]}`; } else { - msg += `${key} : ${value} `; + msg += `${key.toUpperCase()}: ${value} `; } }); Alert(defaultMsg.hdr, msg); diff --git a/awx/ui/client/src/shared/config/config.service.js b/awx/ui/client/src/shared/config/config.service.js index 910e89c9e4..8432303f8e 100644 --- a/awx/ui/client/src/shared/config/config.service.js +++ b/awx/ui/client/src/shared/config/config.service.js @@ -58,6 +58,11 @@ export default deferred.reject('Config not found.'); } return deferred.promise; + }, + + getSubscriptions: function(username, password) { + Rest.setUrl(`${GetBasePath('config')}subscriptions`); + return Rest.post({ rh_username: username, rh_password: password} ); } }; } diff --git a/awx/ui/test/spec/license/license.controller-test.js b/awx/ui/test/spec/license/license.controller-test.js index eb013776b4..660e918e79 100644 --- a/awx/ui/test/spec/license/license.controller-test.js +++ b/awx/ui/test/spec/license/license.controller-test.js @@ -6,7 +6,8 @@ describe('Controller: LicenseController', () => { LicenseController, ConfigService, ProcessErrors, - config; + config, + rhCreds; beforeEach(angular.mock.module('awApp')); beforeEach(angular.mock.module('license', ($provide) => { @@ -22,23 +23,31 @@ describe('Controller: LicenseController', () => { version: '3.1.0-devel' }; + rhCreds = { + password: '$encrypted$', + username: 'foo', + } + ProcessErrors = jasmine.createSpy('ProcessErrors'); $provide.value('ConfigService', ConfigService); $provide.value('ProcessErrors', ProcessErrors); $provide.value('config', config); + $provide.value('rhCreds', rhCreds); })); - beforeEach(angular.mock.inject( ($rootScope, $controller, _ConfigService_, _ProcessErrors_, _config_) => { + beforeEach(angular.mock.inject( ($rootScope, $controller, _ConfigService_, _ProcessErrors_, _config_, _rhCreds_) => { scope = $rootScope.$new(); ConfigService = _ConfigService_; ProcessErrors = _ProcessErrors_; config = _config_; + rhCreds = _rhCreds_; LicenseController = $controller('licenseController', { $scope: scope, ConfigService: ConfigService, ProcessErrors: ProcessErrors, - config: config + config: config, + rhCreds: rhCreds }); })); diff --git a/awxkit/awxkit/api/pages/__init__.py b/awxkit/awxkit/api/pages/__init__.py index 19cf7323ca..fafe5dc08f 100644 --- a/awxkit/awxkit/api/pages/__init__.py +++ b/awxkit/awxkit/api/pages/__init__.py @@ -38,3 +38,4 @@ from .instances import * # NOQA from .instance_groups import * # NOQA from .credential_input_sources import * # NOQA from .metrics import * # NOQA +from .subscriptions import * # NOQA diff --git a/awxkit/awxkit/api/pages/subscriptions.py b/awxkit/awxkit/api/pages/subscriptions.py new file mode 100644 index 0000000000..749776c000 --- /dev/null +++ b/awxkit/awxkit/api/pages/subscriptions.py @@ -0,0 +1,11 @@ +from awxkit.api.resources import resources +from . import page + + +class Subscriptions(page.Page): + + def get_possible_licenses(self, **kwargs): + return self.post(json=kwargs).json + + +page.register_page(resources.subscriptions, Subscriptions) diff --git a/awxkit/awxkit/api/resources.py b/awxkit/awxkit/api/resources.py index 657a41b7f3..d317bcc55d 100644 --- a/awxkit/awxkit/api/resources.py +++ b/awxkit/awxkit/api/resources.py @@ -265,6 +265,7 @@ class Resources(object): _workflow_job_template_workflow_nodes = r'workflow_job_templates/\d+/workflow_nodes/' _workflow_job_templates = 'workflow_job_templates/' _workflow_job_workflow_nodes = r'workflow_jobs/\d+/workflow_nodes/' + _subscriptions = 'config/subscriptions/' _workflow_jobs = 'workflow_jobs/' api = '/api/' common = api + r'v\d+/'