diff --git a/Makefile b/Makefile index 8665341cb1..ecc7e69210 100644 --- a/Makefile +++ b/Makefile @@ -651,7 +651,7 @@ reprepro: deb-build/$(DEB_NVRA).deb reprepro/conf $(REPREPRO_BIN) $(REPREPRO_OPTS) clearvanished for COMPONENT in non-free $(VERSION); do \ $(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT remove $(DEB_DIST) $(NAME) ; \ - $(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT --ignore=brokenold includedeb $(DEB_DIST) deb-build/$(DEB_NVRA).deb ; \ + $(REPREPRO_BIN) $(REPREPRO_OPTS) -C $$COMPONENT --keepunreferencedfiles --ignore=brokenold includedeb $(DEB_DIST) deb-build/$(DEB_NVRA).deb ; \ done diff --git a/awx/__init__.py b/awx/__init__.py index 0a56dd220a..d506b6f1c0 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -6,7 +6,7 @@ import sys import warnings import site -__version__ = '2.4.1' +__version__ = '2.4.2' __all__ = ['__version__'] diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 1ab055d8af..bf258cc524 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1505,7 +1505,7 @@ class JobTemplateSerializer(UnifiedJobTemplateSerializer, JobOptionsSerializer): else: d['can_copy'] = False d['can_edit'] = False - d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.filter(active=True).order_by('-started')[:10]] + d['recent_jobs'] = [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in obj.jobs.filter(active=True).order_by('-created')[:10]] return d def validate_survey_enabled(self, attrs, source): diff --git a/awx/fact/utils/connection.py b/awx/fact/utils/connection.py index 75fe897b3b..4c4019e24d 100644 --- a/awx/fact/utils/connection.py +++ b/awx/fact/utils/connection.py @@ -4,6 +4,7 @@ from django.conf import settings from mongoengine import connect from mongoengine.connection import ConnectionError +from pymongo.errors import AutoReconnect def test_mongo_connection(): # Connect to Mongo @@ -14,13 +15,14 @@ def test_mongo_connection(): raise ConnectionError # Attempt to connect to the MongoDB database. - connect(settings.MONGO_DB, - host=settings.MONGO_HOST, - port=int(settings.MONGO_PORT), - username=settings.MONGO_USERNAME, - password=settings.MONGO_PASSWORD, - tz_aware=settings.USE_TZ) + db = connect(settings.MONGO_DB, + host=settings.MONGO_HOST, + port=int(settings.MONGO_PORT), + username=settings.MONGO_USERNAME, + password=settings.MONGO_PASSWORD, + tz_aware=settings.USE_TZ) + db[settings.MONGO_DB].command('ping') return True - except ConnectionError: + except (ConnectionError, AutoReconnect): return False diff --git a/awx/main/access.py b/awx/main/access.py index 4af42b28f2..18ae3a91b1 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -750,6 +750,9 @@ class ProjectUpdateAccess(BaseAccess): def can_cancel(self, obj): return self.can_change(obj, {}) and obj.can_cancel + def can_delete(self, obj): + return obj and self.user.can_access(Project, 'delete', obj.project) + class PermissionAccess(BaseAccess): ''' I can see a permission when: diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 3377ec8cb6..f73758ad7d 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -5,7 +5,7 @@ import logging import threading import uuid -from django.contrib.auth.models import User, AnonymousUser +from django.contrib.auth.models import User from django.db.models.signals import post_save from django.db import IntegrityError from django.http import HttpResponseRedirect diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index dddd91dfc8..ebc6a0fa78 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -293,7 +293,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): # Overwrite with job template extra vars with survey default vars if self.survey_enabled and 'spec' in self.survey_spec: for survey_element in self.survey_spec.get("spec", []): - if survey_element['default']: + if 'default' in survey_element and survey_element['default']: extra_vars[survey_element['variable']] = survey_element['default'] # transform to dict diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 93303f511c..eef1590d1a 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1101,6 +1101,7 @@ class RunInventoryUpdate(BaseTask): ec2_opts.setdefault('all_rds_instances', 'False') ec2_opts.setdefault('rds', 'False') ec2_opts.setdefault('nested_groups', 'True') + ec2_opts.setdefault('elasticache', 'False') if inventory_update.instance_filters: ec2_opts.setdefault('instance_filters', inventory_update.instance_filters) group_by = [x.strip().lower() for x in inventory_update.group_by.split(',') if x.strip()] diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index f698267a0c..d12fd89e7b 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -1321,6 +1321,28 @@ class ProjectUpdatesTest(BaseTransactionTest): with self.current_user(self.super_django_user): self.post(projects_url, project_data, expect=201) + def test_delete_project_update_as_org_admin(self): + scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS', + 'https://github.com/ansible/ansible.github.com.git') + if not all([scm_url]): + self.skipTest('no public git repo defined for https!') + projects_url = reverse('api:project_list') + project_data = { + 'name': 'my public git project over https', + 'scm_type': 'git', + 'scm_url': scm_url, + } + org = self.make_organizations(self.super_django_user, 1)[0] + org.admins.add(self.normal_django_user) + with self.current_user(self.super_django_user): + del_proj = self.post(projects_url, project_data, expect=201) + del_proj = Project.objects.get(pk=del_proj["id"]) + org.projects.add(del_proj) + pu = self.check_project_update(del_proj) + pu_url = reverse('api:project_update_detail', args=(pu.id,)) + self.delete(pu_url, expect=403, auth=self.get_other_credentials()) + self.delete(pu_url, expect=204, auth=self.get_normal_credentials()) + def test_public_git_project_over_https(self): scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS', 'https://github.com/ansible/ansible.github.com.git') diff --git a/awx/playbooks/scan_facts.yml b/awx/playbooks/scan_facts.yml index 2c51c50709..1b90380c62 100644 --- a/awx/playbooks/scan_facts.yml +++ b/awx/playbooks/scan_facts.yml @@ -4,6 +4,7 @@ scan_use_recursive: false tasks: - scan_packages: + os_family: '{{ ansible_os_family }}' - scan_services: - scan_files: paths: '{{ scan_file_paths }}' diff --git a/awx/plugins/library/scan_packages.py b/awx/plugins/library/scan_packages.py index 8077cfe45d..ee091db39f 100755 --- a/awx/plugins/library/scan_packages.py +++ b/awx/plugins/library/scan_packages.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -import os from ansible.module_utils.basic import * # noqa DOCUMENTATION = ''' @@ -70,16 +69,20 @@ def deb_package_list(): def main(): module = AnsibleModule( - argument_spec = dict()) - - packages = [] - # TODO: module_utils/basic.py in ansible contains get_distribution() and get_distribution_version() - # which can be used here and is accessible by this script instead of this basic detector. - if os.path.exists("/etc/redhat-release"): + argument_spec = dict(os_family=dict(required=True)) + ) + ans_os = module.params['os_family'] + if ans_os in ('RedHat', 'Suse', 'openSUSE Leap'): packages = rpm_package_list() - elif os.path.exists("/etc/os-release"): + elif ans_os == 'Debian': packages = deb_package_list() - results = dict(ansible_facts=dict(packages=packages)) + else: + packages = None + + if packages is not None: + results = dict(ansible_facts=dict(packages=packages)) + else: + results = dict(skipped=True, msg="Unsupported Distribution") module.exit_json(**results) main() diff --git a/awx/ui/client/src/forms/JobTemplates.js b/awx/ui/client/src/forms/JobTemplates.js index 6d3a82286d..6bff4c4b86 100644 --- a/awx/ui/client/src/forms/JobTemplates.js +++ b/awx/ui/client/src/forms/JobTemplates.js @@ -205,7 +205,7 @@ export default column: 2, awPopOver: "
Provide a comma separated list of tags.
\n" + "Tags are useful when you have a large playbook, and you want to run a specific part of a play or task.
" + - "For example, you might have a task consisiting of a long list of actions. Tag values can be assigned to each action. " + + "
For example, you might have a task consisting of a long list of actions. Tag values can be assigned to each action. " + "Suppose the actions have been assigned tag values of "configuration", "packages" and "install".
" + "If you just want to run the "configuration" and "packages" actions, you would enter the following here " + "in the Job Tags field:
\nconfiguration,packages\n", diff --git a/awx/ui/client/src/helpers/Groups.js b/awx/ui/client/src/helpers/Groups.js index 4d7fb0487c..6c2bd6a6c5 100644 --- a/awx/ui/client/src/helpers/Groups.js +++ b/awx/ui/client/src/helpers/Groups.js @@ -790,6 +790,11 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name ParseTypeChange({ scope: properties_scope, field_id: textareaID, onReady: waitStop }); } + function initSourceChange() { + parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=="manual") ? true : false; + SourceChange({ scope: sources_scope, form: SourceForm }); + } + // Set modal dimensions based on viewport width ww = $(document).width(); wh = $('body').height(); @@ -1058,7 +1063,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name } } - sources_scope.sourceChange(); //set defaults that rely on source value + initSourceChange(); if (data.source_regions) { if (data.source === 'ec2' || @@ -1470,8 +1475,7 @@ angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name if (sources_scope.credential_name_api_error) { delete sources_scope.credential_name_api_error; } - parent_scope.showSchedulesTab = (mode === 'edit' && sources_scope.source && sources_scope.source.value!=="manual") ? true : false; - SourceChange({ scope: sources_scope, form: SourceForm }); + initSourceChange(); }; }; diff --git a/awx/ui/client/src/helpers/Jobs.js b/awx/ui/client/src/helpers/Jobs.js index dac06e03c6..cd3bb677af 100644 --- a/awx/ui/client/src/helpers/Jobs.js +++ b/awx/ui/client/src/helpers/Jobs.js @@ -428,7 +428,7 @@ export default // }, 300); }); - if (base === 'jobs' && list.name === 'completed_jobs') { + if (base === 'jobs' && list.name === 'all_jobs') { if ($routeParams.id__int) { scope[list.iterator + 'SearchField'] = 'id'; scope[list.iterator + 'SearchValue'] = $routeParams.id__int; diff --git a/awx/ui/client/src/login/loginModal/loginModal.controller.js b/awx/ui/client/src/login/loginModal/loginModal.controller.js index a2aefb3fdb..d27002d16c 100644 --- a/awx/ui/client/src/login/loginModal/loginModal.controller.js +++ b/awx/ui/client/src/login/loginModal/loginModal.controller.js @@ -115,7 +115,7 @@ export default ['$log', '$cookieStore', '$compile', '$window', '$rootScope', '$l scope.customLogo = ($AnsibleConfig.custom_logo) ? "custom_console_logo.png" : "tower_console_logo.png"; scope.customLoginInfo = $AnsibleConfig.custom_login_info; - scope.customLoginInfoPresent = ($AnsibleConfig.customLoginInfo) ? true : false; + scope.customLoginInfoPresent = (scope.customLoginInfo) ? true : false; }); // Reset the login form diff --git a/awx/ui/client/src/rest/restServices.factory.js b/awx/ui/client/src/rest/restServices.factory.js index 0d03c7ef9e..32473c2a1b 100644 --- a/awx/ui/client/src/rest/restServices.factory.js +++ b/awx/ui/client/src/rest/restServices.factory.js @@ -258,7 +258,8 @@ export default return $http({ method: 'OPTIONS', url: this.url, - headers: this.headers + headers: this.headers, + data: '' }); } else { return this.createResponse({ diff --git a/awx/ui/client/src/system-tracking/system-tracking.partial.html b/awx/ui/client/src/system-tracking/system-tracking.partial.html index cefe3bd7e1..01460f49a8 100644 --- a/awx/ui/client/src/system-tracking/system-tracking.partial.html +++ b/awx/ui/client/src/system-tracking/system-tracking.partial.html @@ -54,7 +54,7 @@ 'is-active': module.isActive, }" ng-click="setActiveModule(module.name)" - ng-repeat="module in modules | orderBy: 'sortKey'"> + ng-repeat="module in modules | orderBy: 'sortKey' track by $index"> {{module.displayName}}