diff --git a/awx/api/filters.py b/awx/api/filters.py index 0a243f9d33..d01bded7f9 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -39,9 +39,11 @@ class V1CredentialFilterBackend(BaseFilterBackend): ''' def filter_queryset(self, request, queryset, view): + # TODO: remove in 3.3 from awx.api.versioning import get_request_version if get_request_version(request) == 1: queryset = queryset.filter(credential_type__managed_by_tower=True) + queryset = queryset.filter(~Q(credential_type__kind='insights')) return queryset diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 2e5fff3e0a..8096799db0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1390,7 +1390,9 @@ class GroupSerializer(BaseSerializerWithVariables): def create(self, validated_data): # TODO: remove in 3.3 instance = super(GroupSerializer, self).create(validated_data) if self.version == 1: # TODO: remove in 3.3 - InventorySource.objects.create(deprecated_group=instance, inventory=instance.inventory) + manual_src = InventorySource(deprecated_group=instance, inventory=instance.inventory) + manual_src.v1_group_name = instance.name + manual_src.save() return instance def validate_name(self, value): diff --git a/awx/api/views.py b/awx/api/views.py index 3b2d489186..074896002f 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2119,7 +2119,7 @@ class HostInsights(GenericAPIView): if host.inventory and host.inventory.insights_credential: cred = host.inventory.insights_credential else: - return Response(dict(error=_('No Insights Credential found for the Inventory, "{}", that this host belongs to.').format(host.inventory.name)), status=status.HTTP_404_NOT_FOUND) + return Response(dict(error=_('The Insights Credential for "{}" was not found.').format(host.inventory.name)), status=status.HTTP_404_NOT_FOUND) url = settings.INSIGHTS_URL_BASE + '/r/insights/v3/systems/{}/reports/'.format(host.insights_system_id) (username, password) = self._extract_insights_creds(cred) diff --git a/awx/main/migrations/_scan_jobs.py b/awx/main/migrations/_scan_jobs.py index c3939018f7..b953ba7d68 100644 --- a/awx/main/migrations/_scan_jobs.py +++ b/awx/main/migrations/_scan_jobs.py @@ -1,24 +1,38 @@ import logging +from django.utils.timezone import now +from django.utils.text import slugify + from awx.main.models.base import PERM_INVENTORY_SCAN, PERM_INVENTORY_DEPLOY +from awx.main import utils + logger = logging.getLogger('awx.main.migrations') -def _create_fact_scan_project(Project, org): +def _create_fact_scan_project(ContentType, Project, org): + ct = ContentType.objects.get_for_model(Project) name = "Tower Fact Scan - {}".format(org.name if org else "No Organization") proj = Project(name=name, scm_url='https://github.com/ansible/tower-fact-modules', scm_type='git', scm_update_on_launch=True, scm_update_cache_timeout=86400, - organization=org) + organization=org, + created=now(), + modified=now(), + polymorphic_ctype=ct) + proj.save() + + slug_name = slugify(unicode(name)).replace(u'-', u'_') + proj.local_path = u'_%d__%s' % (int(proj.pk), slug_name) + proj.save() return proj -def _create_fact_scan_projects(Project, orgs): - return {org.id : _create_fact_scan_project(Project, org) for org in orgs} +def _create_fact_scan_projects(ContentType, Project, orgs): + return {org.id : _create_fact_scan_project(ContentType, Project, org) for org in orgs} def _get_tower_scan_job_templates(JobTemplate): @@ -31,9 +45,10 @@ def _get_orgs(Organization, job_template_ids): def _migrate_scan_job_templates(apps): - Organization = apps.get_model('main', 'Organization') - Project = apps.get_model('main', 'Project') JobTemplate = apps.get_model('main', 'JobTemplate') + Organization = apps.get_model('main', 'Organization') + ContentType = apps.get_model('contenttypes', 'ContentType') + Project = apps.get_model('main', 'Project') project_no_org = None @@ -50,16 +65,17 @@ def _migrate_scan_job_templates(apps): if orgs.count() == 0: return - org_proj_map = _create_fact_scan_projects(Project, orgs) + org_proj_map = _create_fact_scan_projects(ContentType, Project, orgs) for jt in jts: if jt.inventory and jt.inventory.organization: - jt.project = org_proj_map[jt.inventory.organization.id] + jt.project_id = org_proj_map[jt.inventory.organization.id].id # Job Templates without an Organization; through related Inventory else: if not project_no_org: - project_no_org = _create_fact_scan_project(Project, None) - jt.project = project_no_org + project_no_org = _create_fact_scan_project(ContentType, Project, None) + jt.project_id = project_no_org.id jt.job_type = PERM_INVENTORY_DEPLOY + jt.playbook = "scan_facts.yml" jt.use_fact_cache = True jt.save() diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index f88f838087..c482e9b848 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -379,10 +379,10 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin): from awx.main.tasks import delete_inventory if self.pending_deletion is True: raise RuntimeError("Inventory is already pending deletion.") - self.websocket_emit_status('pending_deletion') - delete_inventory.delay(self.pk) self.pending_deletion = True self.save(update_fields=['pending_deletion']) + self.websocket_emit_status('pending_deletion') + delete_inventory.delay(self.pk) class SmartInventoryMembership(BaseModel): @@ -1249,10 +1249,11 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): replace_text = '__replace_%s__' % now() old_name_re = re.compile(r'^inventory_source \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.*?$') if not self.name or old_name_re.match(self.name) or '__replace_' in self.name: + group_name = getattr(self, 'v1_group_name', '') if self.inventory and self.pk: - self.name = '%s (%s)' % (self.inventory.name, self.pk) + self.name = '%s (%s - %s)' % (group_name, self.inventory.name, self.pk) elif self.inventory: - self.name = '%s (%s)' % (self.inventory.name, replace_text) + self.name = '%s (%s - %s)' % (group_name, self.inventory.name, replace_text) elif not is_new_instance: self.name = 'inventory source (%s)' % self.pk else: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3ffb46648e..54401af49f 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1670,6 +1670,11 @@ class RunInventoryUpdate(BaseTask): cp.set(section, 'group_by_resource_group', 'yes') cp.set(section, 'group_by_location', 'yes') cp.set(section, 'group_by_tag', 'yes') + if inventory_update.source_regions: + cp.set( + section, 'locations', + ','.join([x.strip() for x in inventory_update.source_regions.split(',')]) + ) # Return INI content. if cp.sections(): @@ -1747,6 +1752,7 @@ class RunInventoryUpdate(BaseTask): env['AZURE_SUBSCRIPTION_ID'] = passwords.get('source_subscription', '') env['AZURE_AD_USER'] = passwords.get('source_username', '') env['AZURE_PASSWORD'] = passwords.get('source_password', '') + env['AZURE_INI_PATH'] = cloud_credential elif inventory_update.source == 'gce': env['GCE_EMAIL'] = passwords.get('source_username', '') env['GCE_PROJECT'] = passwords.get('source_project', '') diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index e1b5cbe8f9..204b44e03c 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -67,6 +67,29 @@ def test_filter_by_v1_kind_with_vault(get, admin, organization): assert response.data['count'] == 2 +@pytest.mark.django_db +def test_insights_credentials_not_in_v1_api_list(get, admin, organization): + credential_type = CredentialType.defaults['insights']() + credential_type.save() + cred = Credential( + credential_type=credential_type, + name='Best credential ever', + organization=organization, + inputs={ + 'username': u'joe', + 'password': u'secret' + } + ) + cred.save() + + response = get( + reverse('api:credential_list', kwargs={'version': 'v1'}), + admin + ) + assert response.status_code == 200 + assert response.data['count'] == 0 + + @pytest.mark.django_db def test_custom_credentials_not_in_v1_api_list(get, admin, organization): """ diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index 08f6b9b827..3774668b35 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -199,5 +199,5 @@ class TestHostInsights(): resp = view.get(None) - assert resp.data['error'] == 'No Insights Credential found for the Inventory, "inventory_name_here", that this host belongs to.' + assert resp.data['error'] == 'The Insights Credential for "inventory_name_here" was not found.' assert resp.status_code == 404 diff --git a/awx/plugins/library/scan_packages.py b/awx/plugins/library/scan_packages.py index d5aafc66e6..5195a051a6 100755 --- a/awx/plugins/library/scan_packages.py +++ b/awx/plugins/library/scan_packages.py @@ -18,32 +18,45 @@ EXAMPLES = ''' # Example fact output: # host | success >> { # "ansible_facts": { -# "services": [ -# { -# "source": "apt", -# "version": "1.0.6-5", -# "arch": "amd64", -# "name": "libbz2-1.0" -# }, -# { -# "source": "apt", -# "version": "2.7.1-4ubuntu1", -# "arch": "amd64", -# "name": "patch" -# }, -# { -# "source": "apt", -# "version": "4.8.2-19ubuntu1", -# "arch": "amd64", -# "name": "gcc-4.8-base" -# }, ... ] } } +# "packages": { +# "libbz2-1.0": [ +# { +# "version": "1.0.6-5", +# "source": "apt", +# "arch": "amd64", +# "name": "libbz2-1.0" +# } +# ], +# "patch": [ +# { +# "version": "2.7.1-4ubuntu1", +# "source": "apt", +# "arch": "amd64", +# "name": "patch" +# } +# ], +# "gcc-4.8-base": [ +# { +# "version": "4.8.2-19ubuntu1", +# "source": "apt", +# "arch": "amd64", +# "name": "gcc-4.8-base" +# }, +# { +# "version": "4.9.2-19ubuntu1", +# "source": "apt", +# "arch": "amd64", +# "name": "gcc-4.8-base" +# } +# ] +# } ''' def rpm_package_list(): import rpm trans_set = rpm.TransactionSet() - installed_packages = [] + installed_packages = {} for package in trans_set.dbMatch(): package_details = dict(name=package[rpm.RPMTAG_NAME], version=package[rpm.RPMTAG_VERSION], @@ -51,7 +64,10 @@ def rpm_package_list(): epoch=package[rpm.RPMTAG_EPOCH], arch=package[rpm.RPMTAG_ARCH], source='rpm') - installed_packages.append(package_details) + if package_details['name'] not in installed_packages: + installed_packages[package_details['name']] = [package_details] + else: + installed_packages[package_details['name']].append(package_details) return installed_packages @@ -66,7 +82,10 @@ def deb_package_list(): version=ac_pkg.version, arch=ac_pkg.architecture, source='apt') - installed_packages.append(package_details) + if package_details['name'] not in installed_packages: + installed_packages[package_details['name']] = [package_details] + else: + installed_packages[package_details['name']].append(package_details) return installed_packages diff --git a/awx/plugins/library/scan_services.py b/awx/plugins/library/scan_services.py index 11a8edc745..891a3706bf 100644 --- a/awx/plugins/library/scan_services.py +++ b/awx/plugins/library/scan_services.py @@ -20,27 +20,19 @@ EXAMPLES = ''' # Example fact output: # host | success >> { # "ansible_facts": { -# "services": [ -# { -# "name": "acpid", -# "source": "sysv", -# "state": "running" +# "services": { +# "network": { +# "source": "sysv", +# "state": "running", +# "name": "network" # }, -# { -# "name": "apparmor", -# "source": "sysv", -# "state": "stopped" -# }, -# { -# "name": "atd", -# "source": "sysv", -# "state": "running" -# }, -# { -# "name": "cron", -# "source": "sysv", -# "state": "running" -# }, .... ] } } +# "arp-ethers.service": { +# "source": "systemd", +# "state": "stopped", +# "name": "arp-ethers.service" +# } +# } +# } ''' @@ -54,7 +46,7 @@ class BaseService(object): class ServiceScanService(BaseService): def gather_services(self): - services = [] + services = {} service_path = self.module.get_bin_path("service") if service_path is None: return None @@ -73,7 +65,7 @@ class ServiceScanService(BaseService): service_state = "running" else: service_state = "stopped" - services.append({"name": service_name, "state": service_state, "source": "sysv"}) + services[service_name] = {"name": service_name, "state": service_state, "source": "sysv"} # Upstart if initctl_path is not None and chkconfig_path is None: @@ -92,7 +84,7 @@ class ServiceScanService(BaseService): else: pid = None # NOQA payload = {"name": service_name, "state": service_state, "goal": service_goal, "source": "upstart"} - services.append(payload) + services[service_name] = payload # RH sysvinit elif chkconfig_path is not None: @@ -134,7 +126,7 @@ class ServiceScanService(BaseService): else: service_state = 'stopped' service_data = {"name": service_name, "state": service_state, "source": "sysv"} - services.append(service_data) + services[service_name] = service_data return services @@ -153,7 +145,7 @@ class SystemctlScanService(BaseService): return False def gather_services(self): - services = [] + services = {} if not self.systemd_enabled(): return None systemctl_path = self.module.get_bin_path("systemctl", opt_dirs=["/usr/bin", "/usr/local/bin"]) @@ -168,22 +160,20 @@ class SystemctlScanService(BaseService): state_val = "running" else: state_val = "stopped" - services.append({"name": line_data[0], - "state": state_val, - "source": "systemd"}) + services[line_data[0]] = {"name": line_data[0], "state": state_val, "source": "systemd"} return services def main(): module = AnsibleModule(argument_spec = dict()) service_modules = (ServiceScanService, SystemctlScanService) - all_services = [] + all_services = {} incomplete_warning = False for svc_module in service_modules: svcmod = svc_module(module) svc = svcmod.gather_services() if svc is not None: - all_services += svc + all_services.update(svc) if svcmod.incomplete_warning: incomplete_warning = True if len(all_services) == 0: diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 86e0956e3a..d076e234ea 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -777,30 +777,31 @@ GCE_INSTANCE_ID_VAR = None # It's not possible to get zones in Azure without authenticating, so we # provide a list here. AZURE_REGION_CHOICES = [ - ('Central_US', _('US Central')), - ('East_US_1', _('US East')), - ('East_US_2', _('US East 2')), - ('North_Central_US', _('US North Central')), - ('South_Central_US', _('US South Central')), - ('West_Central_US', _('US West Central')), - ('West_US', _('US West')), - ('East_Canada', _('Canada East')), - ('Central_Canada', _('Canada Central')), - ('South_Brazil', _('Brazil South')), - ('North_Europe', _('Europe North')), - ('West_Europe', _('Europe West')), - ('West_UK', _('UK West')), - ('South_UK', _('UK South')), - ('East_Asia', _('Asia East')), - ('Southest_Asia', _('Asia Southeast')), - ('East_Australia', _('Australia East')), - ('Southest_Australia', _('Australia Southeast')), - ('West_India', _('India West')), - ('South_India', _('India South')), - ('East_Japan', _('Japan East')), - ('West_Japan', _('Japan West')), - ('Central_Korea', _('Korea Central')), - ('South_Korea', _('Korea South')), + ('eastus', _('US East')), + ('eastus2', _('US East 2')), + ('centralus', _('US Central')), + ('northcentralus', _('US North Central')), + ('southcentralus', _('US South Central')), + ('westcentralus', _('US West Central')), + ('westus', _('US West')), + ('westus2', _('US West 2')), + ('canadaeast', _('Canada East')), + ('canadacentral', _('Canada Central')), + ('brazilsouth', _('Brazil South')), + ('northeurope', _('Europe North')), + ('westeurope', _('Europe West')), + ('ukwest', _('UK West')), + ('uksouth', _('UK South')), + ('eastasia', _('Asia East')), + ('southestasia', _('Asia Southeast')), + ('australiaeast', _('Australia East')), + ('australiasoutheast', _('Australia Southeast')), + ('westindia', _('India West')), + ('southindia', _('India South')), + ('japaneast', _('Japan East')), + ('japanwest', _('Japan West')), + ('koreacentral', _('Korea Central')), + ('koreasouth', _('Korea South')), ] AZURE_REGIONS_BLACKLIST = [] diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index 92657b68c9..d8ca6d97f9 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -55,6 +55,7 @@ export default [ var formTracker = $scope.$parent.vm.formTracker; var dropdownValue = 'azure'; var activeAuthForm = 'azure'; + let codeInputInitialized = false; // Default active form if ($stateParams.currentTab === '' || $stateParams.currentTab === 'auth') { @@ -244,8 +245,6 @@ export default [ // Flag to avoid re-rendering and breaking Select2 dropdowns on tab switching var dropdownRendered = false; - - function populateLDAPGroupType(flag){ if($scope.$parent.AUTH_LDAP_GROUP_TYPE !== null) { $scope.$parent.AUTH_LDAP_GROUP_TYPE = _.find($scope.$parent.AUTH_LDAP_GROUP_TYPE_options, { value: $scope.$parent.AUTH_LDAP_GROUP_TYPE }); @@ -292,12 +291,24 @@ export default [ populateTacacsProtocol(flag); }); - $scope.$on('codeMirror_populated', function(e, key) { - startCodeMirrors(key); + $scope.$on('$locationChangeStart', (event, url) => { + let parts = url.split('/'); + let tab = parts[parts.length - 1]; + + if (tab === 'auth' && !codeInputInitialized) { + startCodeMirrors(); + codeInputInitialized = true; + } }); $scope.$on('populated', function() { - startCodeMirrors(); + let tab = $stateParams.currentTab; + + if (tab === 'auth') { + startCodeMirrors(); + codeInputInitialized = true; + } + populateLDAPGroupType(false); populateTacacsProtocol(false); }); diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js index ca14b35225..b70e3d71f9 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.controller.js @@ -8,28 +8,35 @@ export default [ '$scope', '$rootScope', '$state', + '$stateParams', '$timeout', 'ConfigurationJobsForm', 'ConfigurationService', 'ConfigurationUtils', 'CreateSelect2', 'GenerateForm', + 'ParseTypeChange', 'i18n', function( $scope, $rootScope, $state, + $stateParams, $timeout, ConfigurationJobsForm, ConfigurationService, ConfigurationUtils, CreateSelect2, GenerateForm, + ParseTypeChange, i18n ) { - var jobsVm = this; var generator = GenerateForm; var form = ConfigurationJobsForm; + + let tab; + let codeInputInitialized = false; + $scope.$parent.AD_HOC_COMMANDS_options = []; _.each($scope.$parent.configDataResolve.AD_HOC_COMMANDS.default, function(command) { $scope.$parent.AD_HOC_COMMANDS_options.push({ @@ -75,6 +82,18 @@ export default [ // Flag to avoid re-rendering and breaking Select2 dropdowns on tab switching var dropdownRendered = false; + function initializeCodeInput () { + let name = 'AWX_TASK_ENV'; + + ParseTypeChange({ + scope: $scope.$parent, + variable: name, + parseType: 'application/json', + field_id: `configuration_jobs_template_${name}` + }); + + $scope.parseTypeChange('parseType', name); + } function populateAdhocCommand(flag){ $scope.$parent.AD_HOC_COMMANDS = $scope.$parent.AD_HOC_COMMANDS.toString(); @@ -107,22 +126,55 @@ export default [ } } - $scope.$on('AD_HOC_COMMANDS_populated', function(e, data, flag) { - populateAdhocCommand(flag); - }); - - $scope.$on('populated', function() { - populateAdhocCommand(false); - }); - // Fix for bug where adding selected opts causes form to be $dirty and triggering modal // TODO Find better solution for this bug $timeout(function(){ $scope.$parent.configuration_jobs_template_form.$setPristine(); }, 1000); - angular.extend(jobsVm, { + $scope.$on('AD_HOC_COMMANDS_populated', function(e, data, flag) { + populateAdhocCommand(flag); }); + /* + * Controllers for each tab are initialized when configuration is opened. A listener + * on the URL itself is necessary to determine which tab is active. If a non-active + * tab initializes a codemirror, it doesn't display properly until the user navigates + * to the tab and it's been clicked. + */ + $scope.$on('$locationChangeStart', (event, url) => { + let parts = url.split('/'); + tab = parts[parts.length - 1]; + + if (tab === 'jobs' && !codeInputInitialized) { + initializeCodeInput(); + codeInputInitialized = true; + } + }); + + /* + * Necessary to listen for revert clicks and relaunch the codemirror instance. + */ + $scope.$on('codeMirror_populated', () => { + if (tab === 'jobs') { + initializeCodeInput(); + } + }); + + /* + * This event is fired if the user navigates directly to this tab, where the + * $locationChangeStart does not. Watching this and location ensure proper display on + * direct load of this tab or if the user comes from a different tab. + */ + $scope.$on('populated', () => { + tab = $stateParams.currentTab; + + if (tab === 'jobs') { + initializeCodeInput(); + codeInputInitialized = true; + } + + populateAdhocCommand(false); + }); } ]; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js index 75a932a512..caa4d522a9 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js @@ -4,76 +4,82 @@ * All Rights Reserved *************************************************/ - export default ['i18n', function(i18n) { - return { - showHeader: false, - name: 'configuration_jobs_template', - showActions: true, +export default ['i18n', function(i18n) { + return { + showHeader: false, + name: 'configuration_jobs_template', + showActions: true, + fields: { + AD_HOC_COMMANDS: { + type: 'select', + ngOptions: 'command.label for command in AD_HOC_COMMANDS_options track by command.value', + reset: 'AD_HOC_COMMANDS', + multiSelect: true + }, + AWX_PROOT_BASE_PATH: { + type: 'text', + reset: 'AWX_PROOT_BASE_PATH', + }, + SCHEDULE_MAX_JOBS: { + type: 'number', + reset: 'SCHEDULE_MAX_JOBS' + }, + AWX_PROOT_SHOW_PATHS: { + type: 'textarea', + reset: 'AWX_PROOT_SHOW_PATHS', + rows: 6 + }, + AWX_ANSIBLE_CALLBACK_PLUGINS: { + type: 'textarea', + reset: 'AWX_ANSIBLE_CALLBACK_PLUGINS', + rows: 6 + }, + AWX_PROOT_HIDE_PATHS: { + type: 'textarea', + reset: 'AWX_PROOT_HIDE_PATHS', + rows: 6 + }, + AWX_PROOT_ENABLED: { + type: 'toggleSwitch', + }, + DEFAULT_JOB_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_JOB_TIMEOUT', + }, + DEFAULT_INVENTORY_UPDATE_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_INVENTORY_UPDATE_TIMEOUT', + }, + DEFAULT_PROJECT_UPDATE_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_PROJECT_UPDATE_TIMEOUT', + }, + ANSIBLE_FACT_CACHE_TIMEOUT: { + type: 'text', + reset: 'ANSIBLE_FACT_CACHE_TIMEOUT', + }, + AWX_TASK_ENV: { + type: 'textarea', + reset: 'AWX_TASK_ENV', + rows: 6, + codeMirror: true, + class: 'Form-textAreaLabel Form-formGroup--fullWidth' + } + }, + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: i18n._('Revert all to default'), + class: 'Form-resetAll' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +}]; - fields: { - AD_HOC_COMMANDS: { - type: 'select', - ngOptions: 'command.label for command in AD_HOC_COMMANDS_options track by command.value', - reset: 'AD_HOC_COMMANDS', - multiSelect: true - }, - AWX_PROOT_BASE_PATH: { - type: 'text', - reset: 'AWX_PROOT_BASE_PATH', - }, - SCHEDULE_MAX_JOBS: { - type: 'number', - reset: 'SCHEDULE_MAX_JOBS' - }, - AWX_PROOT_SHOW_PATHS: { - type: 'textarea', - reset: 'AWX_PROOT_SHOW_PATHS', - rows: 6 - }, - AWX_ANSIBLE_CALLBACK_PLUGINS: { - type: 'textarea', - reset: 'AWX_ANSIBLE_CALLBACK_PLUGINS', - rows: 6 - }, - AWX_PROOT_HIDE_PATHS: { - type: 'textarea', - reset: 'AWX_PROOT_HIDE_PATHS', - rows: 6 - }, - AWX_PROOT_ENABLED: { - type: 'toggleSwitch', - }, - DEFAULT_JOB_TIMEOUT: { - type: 'text', - reset: 'DEFAULT_JOB_TIMEOUT', - }, - DEFAULT_INVENTORY_UPDATE_TIMEOUT: { - type: 'text', - reset: 'DEFAULT_INVENTORY_UPDATE_TIMEOUT', - }, - DEFAULT_PROJECT_UPDATE_TIMEOUT: { - type: 'text', - reset: 'DEFAULT_PROJECT_UPDATE_TIMEOUT', - }, - ANSIBLE_FACT_CACHE_TIMEOUT: { - type: 'text', - reset: 'ANSIBLE_FACT_CACHE_TIMEOUT', - }, - }, - - buttons: { - reset: { - ngClick: 'vm.resetAllConfirm()', - label: i18n._('Revert all to default'), - class: 'Form-resetAll' - }, - cancel: { - ngClick: 'vm.formCancel()', - }, - save: { - ngClick: 'vm.formSave()', - ngDisabled: true - } - } - }; - }]; diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js index 96c04b7eac..d62933314b 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js @@ -18,8 +18,9 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', init(); function init() { + $scope.mode = 'add'; // apply form definition's default field values - GenerateForm.applyDefaults(form, $scope); + GenerateForm.applyDefaults(form, $scope, true); $scope.canAdd = inventorySourcesOptions.actions.POST; $scope.envParseType = 'yaml'; initSources(); @@ -46,36 +47,6 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', } }; - // Detect and alert user to potential SCM status issues - var checkSCMStatus = function () { - if (!Empty($scope.project)) { - Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); - Rest.get() - .success(function (data) { - var msg; - switch (data.status) { - case 'failed': - msg = "
The Project selected has a status of \"failed\". You must run a successful update before you can select an inventory file."; - break; - case 'never updated': - msg = "
The Project selected has a status of \"never updated\". You must run a successful update before you can select an inventory file."; - break; - case 'missing': - msg = '
The selected project has a status of \"missing\". Please check the server and make sure ' + - ' the directory exists and file permissions are set correctly.
'; - break; - } - if (msg) { - Alert('Warning', msg, 'alert-info alert-info--noTextTransform', null, null, null, null, true); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to get project ' + $scope.project + '. GET returned status: ' + status }); - }); - } - }; - // Register a watcher on project_name if ($scope.getInventoryFilesUnregister) { $scope.getInventoryFilesUnregister(); @@ -83,7 +54,6 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', $scope.getInventoryFilesUnregister = $scope.$watch('project', function (newValue, oldValue) { if (newValue !== oldValue) { getInventoryFiles(newValue); - checkSCMStatus(); } }); @@ -118,7 +88,6 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', }); }; - $scope.projectBasePath = GetBasePath('projects'); $scope.credentialBasePath = GetBasePath('credentials') + '?credential_type__kind__in=cloud,network'; $scope.sourceChange = function(source) { @@ -144,10 +113,11 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', } if (source === 'scm') { - $scope.overwrite_vars = true; - $scope.inventory_source_form.inventory_file.$setPristine(); + $scope.projectBasePath = GetBasePath('projects') + '?not__status=never updated'; + $scope.overwrite_vars = true; + $scope.inventory_source_form.inventory_file.$setPristine(); } else { - $scope.overwrite_vars = false; + $scope.overwrite_vars = false; } // reset fields diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js index f4a68679ed..acef7d2c82 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js @@ -8,15 +8,15 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupsService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'inventorySourceData', 'SourcesService', 'inventoryData', 'inventorySourcesOptions', 'Empty', - 'Wait', 'Rest', 'Alert', 'ProcessErrors', + 'Wait', 'Rest', 'Alert', function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON,ParseTypeChange, GroupsService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, inventorySourceData, SourcesService, inventoryData, inventorySourcesOptions, Empty, - Wait, Rest, Alert, ProcessErrors) { + Wait, Rest, Alert) { function init() { - $scope.projectBasePath = GetBasePath('projects'); + $scope.projectBasePath = GetBasePath('projects') + '?not__status=never updated'; $scope.canAdd = inventorySourcesOptions.actions.POST; // instantiate expected $scope values from inventorySourceData _.assign($scope, @@ -136,33 +136,6 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', Wait('stop'); }); } - - if (!Empty($scope.project)) { - Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); - Rest.get() - .success(function (data) { - var msg; - switch (data.status) { - case 'failed': - msg = "
The Project selected has a status of \"failed\". You must run a successful update before you can select an inventory file."; - break; - case 'never updated': - msg = "
The Project selected has a status of \"never updated\". You must run a successful update before you can select an inventory file."; - break; - case 'missing': - msg = '
The selected project has a status of \"missing\". Please check the server and make sure ' + - ' the directory exists and file permissions are set correctly.
'; - break; - } - if (msg) { - Alert('Warning', msg, 'alert-info alert-info--noTextTransform', null, null, null, null, true); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to get project ' + $scope.project + '. GET returned status: ' + status }); - }); - } } function initSourceSelect() { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-project.route.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-project.route.js index 14614b24d1..53869691a7 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-project.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/lookup/sources-lookup-project.route.js @@ -4,6 +4,7 @@ export default { value: { page_size:"5", order_by:"name", + not__status:"never updated", role_level:"use_role", }, dynamic:true, diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 881c04037b..e66b2b746a 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -89,6 +89,7 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy $scope.machine_credential_link = getTowerLink('credential'); $scope.cloud_credential_link = getTowerLink('cloud_credential'); $scope.network_credential_link = getTowerLink('network_credential'); + $scope.vault_credential_link = getTowerLink('vault_credential'); $scope.schedule_link = getTowerLink('schedule'); }; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index ac5ad64022..ae6b723305 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -323,6 +323,21 @@
+ + +
diff --git a/awx/ui/client/src/job-submission/job-submission.controller.js b/awx/ui/client/src/job-submission/job-submission.controller.js index 5572b860c2..021e44c723 100644 --- a/awx/ui/client/src/job-submission/job-submission.controller.js +++ b/awx/ui/client/src/job-submission/job-submission.controller.js @@ -163,6 +163,7 @@ export default $scope.password_needed = data.passwords_needed_to_start && data.passwords_needed_to_start.length > 0; $scope.has_default_inventory = data.defaults && data.defaults.inventory && data.defaults.inventory.id; $scope.has_default_credential = data.defaults && data.defaults.credential && data.defaults.credential.id; + $scope.has_default_vault_credential = data.defaults && data.defaults.vault_credential && data.defaults.vault_credential.id; $scope.has_default_extra_credentials = data.defaults && data.defaults.extra_credentials && data.defaults.extra_credentials.length > 0; $scope.other_prompt_data = {}; @@ -231,6 +232,10 @@ export default $scope.selected_credentials.machine = angular.copy($scope.defaults.credential); } + if($scope.has_default_vault_credential) { + $scope.selected_credentials.vault = angular.copy($scope.defaults.vault_credential); + } + if($scope.has_default_extra_credentials) { $scope.selected_credentials.extra = angular.copy($scope.defaults.extra_credentials); } @@ -361,6 +366,12 @@ export default else { $scope.selected_credentials.machine = null; } + if($scope.has_default_vault_credential) { + $scope.selected_credentials.vault = angular.copy($scope.defaults.vault_credential); + } + else { + $scope.selected_credentials.vault = null; + } if($scope.has_default_extra_credentials) { $scope.selected_credentials.extra = angular.copy($scope.defaults.extra_credentials); } diff --git a/awx/ui/client/src/job-submission/job-submission.partial.html b/awx/ui/client/src/job-submission/job-submission.partial.html index 37ab037ef5..e421adac7f 100644 --- a/awx/ui/client/src/job-submission/job-submission.partial.html +++ b/awx/ui/client/src/job-submission/job-submission.partial.html @@ -73,6 +73,12 @@ {{credential_types[extraCredential.credential_type].name | uppercase}}: {{extraCredential.name}}
+
+
+ + VAULT: {{selected_credentials.vault.name}} +
+
@@ -167,7 +173,7 @@