diff --git a/Makefile b/Makefile index cab5a6466c..36382bd232 100644 --- a/Makefile +++ b/Makefile @@ -413,11 +413,11 @@ rpm-build/$(RPM_NVR).noarch.rpm: rpm-build/$(RPM_NVR).src.rpm mock-rpm: rpmtar rpm-build/$(RPM_NVR).noarch.rpm ifeq ($(OFFICIAL),yes) - rpm-build/$(GPG_FILE): rpm-build - gpg --export -a "${GPG_KEY}" > "$@" +rpm-build/$(GPG_FILE): rpm-build + gpg --export -a "${GPG_KEY}" > "$@" - rpm-sign: rpm-build/$(GPG_FILE) rpmtar rpm-build/$(RPM_NVR).noarch.rpm - rpm --define "_signature gpg" --define "_gpg_name $(GPG_KEY)" --addsign rpm-build/$(RPM_NVR).noarch.rpm +rpm-sign: rpm-build/$(GPG_FILE) rpmtar rpm-build/$(RPM_NVR).noarch.rpm + rpm --define "_signature gpg" --define "_gpg_name $(GPG_KEY)" --addsign rpm-build/$(RPM_NVR).noarch.rpm endif deb-build/$(SDIST_TAR_NAME): diff --git a/awx/api/views.py b/awx/api/views.py index 10909f0a83..30bd034619 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -273,7 +273,7 @@ class ApiV1ConfigView(APIView): mongodb_control.delay('stop') except OSError: pass - return Response() + return Response(status=status.HTTP_204_NO_CONTENT) class DashboardView(APIView): diff --git a/awx/fact/models/fact.py b/awx/fact/models/fact.py index d89388276e..2ec6a8e6a0 100644 --- a/awx/fact/models/fact.py +++ b/awx/fact/models/fact.py @@ -182,6 +182,7 @@ class FactVersion(Document): meta = { 'indexes': [ '-timestamp', - 'module' + 'module', + 'host', ] } diff --git a/awx/main/management/commands/cleanup_facts.py b/awx/main/management/commands/cleanup_facts.py index 0ff743b15e..709b73066e 100644 --- a/awx/main/management/commands/cleanup_facts.py +++ b/awx/main/management/commands/cleanup_facts.py @@ -31,53 +31,48 @@ class CleanupFacts(object): # pivot -= granularity # group by host def cleanup(self, older_than_abs, granularity, module=None): - flag_delete_all = False fact_oldest = FactVersion.objects.all().order_by('timestamp').first() if not fact_oldest: return 0 + kv = { + 'timestamp__lte': older_than_abs + } + if module: + kv['module'] = module + # Special case, granularity=0x where x is d, w, or y # The intent is to delete all facts < older_than_abs if granularity == relativedelta(): - flag_delete_all = True + return FactVersion.objects.filter(**kv).order_by('-timestamp').delete() total = 0 + date_pivot = older_than_abs while date_pivot > fact_oldest.timestamp: date_pivot_next = date_pivot - granularity + + # For the current time window. + # Delete all facts expect the fact that matches the largest timestamp. kv = { 'timestamp__lte': date_pivot } - if not flag_delete_all: - kv['timestamp__gt'] = date_pivot_next if module: kv['module'] = module - version_objs = FactVersion.objects.filter(**kv).order_by('-timestamp') - if flag_delete_all: - total = version_objs.delete() - break - - # Transform array -> {host_id} = [, , ...] - # TODO: If this set gets large then we can use mongo to transform the data set for us. - host_ids = {} - for obj in version_objs: - k = obj.host.id - if k not in host_ids: - host_ids[k] = [] - host_ids[k].append(obj) - - for k in host_ids: - ids = [fact.id for fact in host_ids[k]] - fact_ids = [fact.fact.id for fact in host_ids[k]] - # Remove 1 entry - ids.pop() - fact_ids.pop() - # delete the rest - count = FactVersion.objects.filter(id__in=ids).delete() - # FIXME: if this crashes here then we are inconsistent - count = Fact.objects.filter(id__in=fact_ids).delete() + fact_version_objs = FactVersion.objects.filter(**kv).order_by('-timestamp').limit(1) + if fact_version_objs: + fact_version_obj = fact_version_objs[0] + kv = { + 'timestamp__lt': fact_version_obj.timestamp, + 'timestamp__gt': date_pivot_next + } + if module: + kv['module'] = module + count = FactVersion.objects.filter(**kv).delete() + # FIXME: These two deletes should be a transaction + count = Fact.objects.filter(**kv).delete() total += count date_pivot = date_pivot_next diff --git a/awx/main/tests/commands/cleanup_facts.py b/awx/main/tests/commands/cleanup_facts.py index 4d40acbcb2..53f4b29187 100644 --- a/awx/main/tests/commands/cleanup_facts.py +++ b/awx/main/tests/commands/cleanup_facts.py @@ -21,6 +21,7 @@ __all__ = ['CommandTest','CleanupFactsUnitTest', 'CleanupFactsCommandFunctionalT class CleanupFactsCommandFunctionalTest(BaseCommandMixin, BaseTest, MongoDBRequired): def setUp(self): super(CleanupFactsCommandFunctionalTest, self).setUp() + self.create_test_license_file() self.builder = FactScanBuilder() self.builder.add_fact('ansible', TEST_FACT_ANSIBLE) @@ -55,6 +56,10 @@ class CleanupFactsCommandFunctionalTest(BaseCommandMixin, BaseTest, MongoDBRequi self.assertEqual(stdout, 'Deleted 25 facts.\n') class CommandTest(BaseTest): + def setUp(self): + super(CommandTest, self).setUp() + self.create_test_license_file() + @mock.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run') def test_parameters_ok(self, run): @@ -149,11 +154,11 @@ class CleanupFactsUnitTest(BaseCommandMixin, BaseTest, MongoDBRequired): self.builder = FactScanBuilder() self.builder.add_fact('ansible', TEST_FACT_ANSIBLE) + self.builder.add_fact('packages', TEST_FACT_PACKAGES) self.builder.build(scan_count=20, host_count=10) ''' - Create 10 hosts with 20 facts each. A single fact a year for 20 years. - After cleanup, there should be 10 facts for each host. + Create 10 hosts with 40 facts each. After cleanup, there should be 20 facts for each host. Then ensure the correct facts are deleted. ''' def test_cleanup_logic(self): @@ -162,17 +167,18 @@ class CleanupFactsUnitTest(BaseCommandMixin, BaseTest, MongoDBRequired): granularity = relativedelta(years=2) deleted_count = cleanup_facts.cleanup(self.builder.get_timestamp(0), granularity) - self.assertEqual(deleted_count, (self.builder.get_scan_count() * self.builder.get_host_count()) / 2) + self.assertEqual(deleted_count, 2 * (self.builder.get_scan_count() * self.builder.get_host_count()) / 2) # Check the number of facts per host for host in self.builder.get_hosts(): count = FactVersion.objects.filter(host=host).count() - self.assertEqual(count, self.builder.get_scan_count() / 2, "should have half the number of FactVersion per host for host %s") + scan_count = (2 * self.builder.get_scan_count()) / 2 + self.assertEqual(count, scan_count) count = Fact.objects.filter(host=host).count() - self.assertEqual(count, self.builder.get_scan_count() / 2, "should have half the number of Fact per host") + self.assertEqual(count, scan_count) - # Ensure that only 1 fact exists per granularity time + # Ensure that only 2 facts (ansible and packages) exists per granularity time date_pivot = self.builder.get_timestamp(0) for host in self.builder.get_hosts(): while date_pivot > fact_oldest.timestamp: @@ -183,11 +189,50 @@ class CleanupFactsUnitTest(BaseCommandMixin, BaseTest, MongoDBRequired): 'host': host, } count = FactVersion.objects.filter(**kv).count() - self.assertEqual(count, 1, "should only be 1 FactVersion per the 2 year granularity") + self.assertEqual(count, 2, "should only be 2 FactVersion per the 2 year granularity") count = Fact.objects.filter(**kv).count() - self.assertEqual(count, 1, "should only be 1 Fact per the 2 year granularity") + self.assertEqual(count, 2, "should only be 2 Fact per the 2 year granularity") + date_pivot = date_pivot_next + + ''' + Create 10 hosts with 40 facts each. After cleanup, there should be 30 facts for each host. + Then ensure the correct facts are deleted. + ''' + def test_cleanup_module(self): + cleanup_facts = CleanupFacts() + fact_oldest = FactVersion.objects.all().order_by('timestamp').first() + granularity = relativedelta(years=2) + + deleted_count = cleanup_facts.cleanup(self.builder.get_timestamp(0), granularity, module='ansible') + self.assertEqual(deleted_count, (self.builder.get_scan_count() * self.builder.get_host_count()) / 2) + + # Check the number of facts per host + for host in self.builder.get_hosts(): + count = FactVersion.objects.filter(host=host).count() + self.assertEqual(count, 30) + + count = Fact.objects.filter(host=host).count() + self.assertEqual(count, 30) + + # Ensure that only 1 ansible fact exists per granularity time + date_pivot = self.builder.get_timestamp(0) + for host in self.builder.get_hosts(): + while date_pivot > fact_oldest.timestamp: + date_pivot_next = date_pivot - granularity + kv = { + 'timestamp__lte': date_pivot, + 'timestamp__gt': date_pivot_next, + 'host': host, + 'module': 'ansible', + } + count = FactVersion.objects.filter(**kv).count() + self.assertEqual(count, 1) + count = Fact.objects.filter(**kv).count() + self.assertEqual(count, 1) date_pivot = date_pivot_next + + diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index c2040eb3b6..3cc7d22350 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -440,6 +440,10 @@ var tower = angular.module('Tower', [ } }). + when('/inventories/:inventory_id/job_templates/', { + redirectTo: '/inventories/:inventory_id' + }). + when('/inventories/:inventory_id/job_templates/:template_id', { name: 'inventoryJobTemplateEdit', templateUrl: urlPrefix + 'partials/job_templates.html', diff --git a/awx/ui/static/js/helpers/License.js b/awx/ui/static/js/helpers/License.js index e027ea80a9..1c6572e60b 100644 --- a/awx/ui/static/js/helpers/License.js +++ b/awx/ui/static/js/helpers/License.js @@ -173,6 +173,7 @@ export default // ignore } Alert('License Accepted', 'The Ansible Tower license was updated. To view or update license information in the future choose View License from the Account menu.','alert-info'); + $rootScope.features = undefined; $location.path('/home'); }) .error(function (data, status) { diff --git a/awx/ui/static/js/main-menu/main-menu.block.less b/awx/ui/static/js/main-menu/main-menu.block.less index 038b98b5a9..932f3a1438 100644 --- a/awx/ui/static/js/main-menu/main-menu.block.less +++ b/awx/ui/static/js/main-menu/main-menu.block.less @@ -29,6 +29,7 @@ @media screen and (min-width: (@menu-breakpoint + 1px)) { padding: 0 20px; + padding-right: 23px; } &-menuContainer { diff --git a/awx/ui/static/js/main-menu/menu-item.directive.js b/awx/ui/static/js/main-menu/menu-item.directive.js index 950abf3cba..7fba3d1440 100644 --- a/awx/ui/static/js/main-menu/menu-item.directive.js +++ b/awx/ui/static/js/main-menu/menu-item.directive.js @@ -14,7 +14,7 @@ export default ['$route', '$rootScope', function($route, $rootScope) { }); scope.$on('$routeChangeSuccess', function(e, nextRoute) { - if (nextRoute.$$route.name === routeName) { + if (nextRoute.$$route && nextRoute.$$route.name === routeName) { element.addClass('MenuItem--active'); } else { element.removeClass('MenuItem--active'); diff --git a/awx/ui/static/js/smart-status/smart-status.controller.js b/awx/ui/static/js/smart-status/smart-status.controller.js index 72b76bbe5b..12ba8022ea 100644 --- a/awx/ui/static/js/smart-status/smart-status.controller.js +++ b/awx/ui/static/js/smart-status/smart-status.controller.js @@ -14,7 +14,7 @@ export default ['$scope', '$filter', } var sparkData = - recentJobs.map(function(job) { + _.sortBy(recentJobs.map(function(job) { var data = {}; @@ -28,10 +28,10 @@ export default ['$scope', '$filter', data.jobId = job.id; data.smartStatus = job.status; - data.finished = $filter('longDate')(job.finished); + data.finished = $filter('longDate')(job.finished) || "running"; return data; - }); + }), "finished").reverse(); $scope.sparkArray = _.pluck(sparkData, 'value'); $scope.jobIds = _.pluck(sparkData, 'jobId'); diff --git a/awx/ui/static/js/smart-status/smart-status.directive.js b/awx/ui/static/js/smart-status/smart-status.directive.js index d220fa7e86..3a59e5a511 100644 --- a/awx/ui/static/js/smart-status/smart-status.directive.js +++ b/awx/ui/static/js/smart-status/smart-status.directive.js @@ -18,10 +18,13 @@ export default [ function() { //capitalize first letter if (status) { status = status.charAt(0).toUpperCase() + status.slice(1); - return "
Job ID: " + + var tooltip = "
Job ID: " + options.userOptions.tooltipValueLookups.jobs[point.offset] + - "
Status: "+status+ - "
Finished: " + finished +"
" ; + "
Status: "+status; + if (finished !== "running") { + tooltip += "
Finished: " + finished +"
" ; + } + return tooltip; } }; diff --git a/awx/ui/static/js/system-tracking/compare-facts.js b/awx/ui/static/js/system-tracking/compare-facts.js index 0d5e77398f..04d1ee3a6d 100644 --- a/awx/ui/static/js/system-tracking/compare-facts.js +++ b/awx/ui/static/js/system-tracking/compare-facts.js @@ -9,7 +9,9 @@ import compareFlatFacts from './compare-facts/flat'; export function compareFacts(module, facts) { if (module.displayType === 'nested') { - return compareNestedFacts(facts); + return { factData: compareNestedFacts(facts), + isNestedDisplay: true + }; } else { // For flat structures we compare left-to-right, then right-to-left to // make sure we get a good comparison between both hosts diff --git a/awx/ui/static/js/system-tracking/compare-facts/nested-helpers.js b/awx/ui/static/js/system-tracking/compare-facts/nested-helpers.js index 37c50d9abd..73063b8f40 100644 --- a/awx/ui/static/js/system-tracking/compare-facts/nested-helpers.js +++ b/awx/ui/static/js/system-tracking/compare-facts/nested-helpers.js @@ -127,8 +127,8 @@ export function formatFacts(diffedResults) { } export function findFacts(factData) { - var rightData = factData[0]; - var leftData = factData[1]; + var rightData = factData[0].facts; + var leftData = factData[1].facts; function factObject(keyPath, key, leftValue, rightValue) { var obj = diff --git a/awx/ui/static/js/system-tracking/system-tracking.controller.js b/awx/ui/static/js/system-tracking/system-tracking.controller.js index 369dddaed1..53d355566b 100644 --- a/awx/ui/static/js/system-tracking/system-tracking.controller.js +++ b/awx/ui/static/js/system-tracking/system-tracking.controller.js @@ -45,11 +45,10 @@ function controller($rootScope, $scope.leftHostname = hosts[0].name; $scope.rightHostname = hosts.length > 1 ? hosts[1].name : hosts[0].name; - function reloadData(params, initialData) { + function reloadData(params) { searchConfig = _.assign({}, searchConfig, params); - var factData = initialData; var leftRange = searchConfig.leftRange; var rightRange = searchConfig.rightRange; var activeModule = searchConfig.module; @@ -80,9 +79,6 @@ function controller($rootScope, // arrays in index 1 // - // Save the position of the data so we - // don't lose it later - var wrappedFacts = facts.map(function(facts, index) { return { position: index === 0 ? 'left' : 'right', diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index 5d3785dadb..de033f7507 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -1984,3 +1984,7 @@ tr td button i { .job-stdout-panel { margin: 0 15px; } + +.panel-title { + font-weight: bold; +} diff --git a/awx/ui/static/less/main-layout.less b/awx/ui/static/less/main-layout.less index e0fc9de3bc..b795e1d506 100644 --- a/awx/ui/static/less/main-layout.less +++ b/awx/ui/static/less/main-layout.less @@ -13,7 +13,7 @@ body { font-family: 'Open Sans', sans-serif; font-weight: 400; color: @black; - padding-top: 75px; + padding-top: 58px; } #main-menu-container { diff --git a/awx/ui/static/partials/job_stdout.html b/awx/ui/static/partials/job_stdout.html index 3f6eed31a1..52b762c2db 100644 --- a/awx/ui/static/partials/job_stdout.html +++ b/awx/ui/static/partials/job_stdout.html @@ -21,7 +21,7 @@
-

{{ job.name }} standard out

+

Standard Out

diff --git a/awx/ui/static/partials/job_stdout_adhoc.html b/awx/ui/static/partials/job_stdout_adhoc.html index 61f6359cee..b2bda4458a 100644 --- a/awx/ui/static/partials/job_stdout_adhoc.html +++ b/awx/ui/static/partials/job_stdout_adhoc.html @@ -146,7 +146,7 @@
-

{{ job.name }} standard out

+

Standard Out