diff --git a/.gitignore b/.gitignore index b452f7ee24..c385ad667d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ __pycache__ /tar-build /setup-bundle-build /dist -*.egg-info +/*.egg-info *.py[c,o] # JavaScript diff --git a/Makefile b/Makefile index 39e76bbc4d..00d8f14ad3 100644 --- a/Makefile +++ b/Makefile @@ -273,9 +273,9 @@ version_file: # Do any one-time init tasks. init: @if [ "$(VIRTUAL_ENV)" ]; then \ - $(PYTHON) manage.py register_instance --primary --hostname=127.0.0.1; \ + tower-manage register_instance --primary --hostname=127.0.0.1; \ else \ - sudo $(PYTHON) manage.py register_instance --primary --hostname=127.0.0.1; \ + sudo tower-manage register_instance --primary --hostname=127.0.0.1; \ fi # Refresh development environment after pulling new code. diff --git a/awx/api/serializers.py b/awx/api/serializers.py index b1f11268eb..71a103a1a6 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -558,7 +558,7 @@ class BaseFactSerializer(BaseSerializer): def get_fields(self): ret = super(BaseFactSerializer, self).get_fields() - if 'module' in ret and feature_enabled('system_tracking'): + if 'module' in ret: # TODO: the values_list may pull in a LOT of entries before the distinct is called modules = Fact.objects.all().values_list('module', flat=True).distinct() choices = [(o, o.title()) for o in modules] diff --git a/awx/api/views.py b/awx/api/views.py index 2a9c2b7228..a30ea1870b 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1289,7 +1289,17 @@ class HostActivityStreamList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(Q(host=parent) | Q(inventory=parent.inventory)) -class HostFactVersionsList(ListAPIView, ParentMixin): +class SystemTrackingEnforcementMixin(APIView): + ''' + Use check_permissions instead of initial() because it's in the OPTION's path as well + ''' + def check_permissions(self, request): + if not feature_enabled("system_tracking"): + raise LicenseForbids("Your license does not permit use " + "of system tracking.") + return super(SystemTrackingEnforcementMixin, self).check_permissions(request) + +class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMixin): model = Fact serializer_class = FactVersionSerializer @@ -1297,10 +1307,6 @@ class HostFactVersionsList(ListAPIView, ParentMixin): new_in_220 = True def get_queryset(self): - if not feature_enabled("system_tracking"): - raise LicenseForbids("Your license does not permit use " - "of system tracking.") - from_spec = self.request.query_params.get('from', None) to_spec = self.request.query_params.get('to', None) module_spec = self.request.query_params.get('module', None) @@ -1318,7 +1324,7 @@ class HostFactVersionsList(ListAPIView, ParentMixin): queryset = self.get_queryset() or [] return Response(dict(results=self.serializer_class(queryset, many=True).data)) -class HostFactCompareView(SubDetailAPIView): +class HostFactCompareView(SubDetailAPIView, SystemTrackingEnforcementMixin): model = Fact new_in_220 = True @@ -1326,11 +1332,6 @@ class HostFactCompareView(SubDetailAPIView): serializer_class = FactSerializer def retrieve(self, request, *args, **kwargs): - # Sanity check: Does the license allow system tracking? - if not feature_enabled('system_tracking'): - raise LicenseForbids('Your license does not permit use ' - 'of system tracking.') - datetime_spec = request.query_params.get('datetime', None) module_spec = request.query_params.get('module', "ansible") datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now() diff --git a/awx/fact/utils/connection.py b/awx/fact/utils/connection.py deleted file mode 100644 index 4c4019e24d..0000000000 --- a/awx/fact/utils/connection.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -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 - try: - # Sanity check: If we have intentionally invalid settings, then we - # know we cannot connect. - if settings.MONGO_HOST == NotImplemented: - raise ConnectionError - - # Attempt to connect to the MongoDB database. - 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, AutoReconnect): - return False - diff --git a/awx/main/management/commands/cleanup_facts.py b/awx/main/management/commands/cleanup_facts.py index 11d5d88996..578bee3441 100644 --- a/awx/main/management/commands/cleanup_facts.py +++ b/awx/main/management/commands/cleanup_facts.py @@ -12,7 +12,7 @@ from django.db import transaction from django.utils.timezone import now # AWX -from awx.fact.models.fact import * # noqa +from awx.main.models.fact import Fact from awx.api.license import feature_enabled OLDER_THAN = 'older_than' @@ -31,7 +31,7 @@ class CleanupFacts(object): # pivot -= granularity # group by host def cleanup(self, older_than_abs, granularity, module=None): - fact_oldest = FactVersion.objects.all().order_by('timestamp').first() + fact_oldest = Fact.objects.all().order_by('timestamp').first() if not fact_oldest: return 0 @@ -44,7 +44,10 @@ class CleanupFacts(object): # Special case, granularity=0x where x is d, w, or y # The intent is to delete all facts < older_than_abs if granularity == relativedelta(): - return FactVersion.objects.filter(**kv).order_by('-timestamp').delete() + qs = Fact.objects.filter(**kv) + count = qs.count() + qs.delete() + return count total = 0 @@ -61,18 +64,17 @@ class CleanupFacts(object): kv['module'] = module - fact_version_objs = FactVersion.objects.filter(**kv).order_by('-timestamp').limit(1) - if fact_version_objs: - fact_version_obj = fact_version_objs[0] + fact_version_obj = Fact.objects.filter(**kv).order_by('-timestamp').first() + if fact_version_obj: 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() + qs = Fact.objects.filter(**kv) + count = qs.count() + qs.delete() total += count date_pivot = date_pivot_next diff --git a/awx/main/management/commands/run_fact_cache_receiver.py b/awx/main/management/commands/run_fact_cache_receiver.py index 42fc25a561..062cd39693 100644 --- a/awx/main/management/commands/run_fact_cache_receiver.py +++ b/awx/main/management/commands/run_fact_cache_receiver.py @@ -67,7 +67,7 @@ class FactCacheReceiver(object): self.timestamp = datetime.fromtimestamp(date_key, None) # Update existing Fact entry - fact_obj = Fact.get_host_fact(host_obj.id, module_name, self.timestamp) + fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp) if fact_obj: fact_obj.facts = facts fact_obj.save() diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 509c5d1e7e..3942cc78bb 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -53,7 +53,6 @@ from awx.main.task_engine import TaskSerializer, TASK_TIMEOUT_INTERVAL from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url, ignore_inventory_computed_fields, emit_websocket_notification, check_proot_installed, build_proot_temp_dir, wrap_args_with_proot) -from awx.fact.utils.connection import test_mongo_connection __all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate', 'RunAdHocCommand', 'handle_work_error', 'handle_work_success', @@ -959,11 +958,6 @@ class RunJob(BaseTask): ''' return getattr(tower_settings, 'AWX_PROOT_ENABLED', False) - def pre_run_hook(self, job, **kwargs): - if job.job_type == PERM_INVENTORY_SCAN: - if not test_mongo_connection(): - raise RuntimeError("Fact Scan Database is offline") - def post_run_hook(self, job, **kwargs): ''' Hook for actions to run after job/task has completed. diff --git a/awx/main/tests/functional/api/test_fact_versions.py b/awx/main/tests/functional/api/test_fact_versions.py index b203c3deff..dfb067a1f8 100644 --- a/awx/main/tests/functional/api/test_fact_versions.py +++ b/awx/main/tests/functional/api/test_fact_versions.py @@ -16,6 +16,9 @@ from django.utils import timezone def mock_feature_enabled(feature, bypass_database=None): return True +def mock_feature_disabled(feature, bypass_database=None): + return False + def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params={}, host_count=1): hosts = hosts(host_count=host_count) fact_scans(fact_scans=3, timestamp_epoch=epoch) @@ -42,8 +45,33 @@ def check_response_facts(facts_known, response): assert timestamp_apiformat(fact_known.timestamp) == response.data['results'][i]['timestamp'] check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module) +def check_system_tracking_feature_forbidden(response): + assert 402 == response.status_code + assert 'Your license does not permit use of system tracking.' == response.data['detail'] + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_get(hosts, get, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) + response = get(url, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_options(hosts, options, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) + response = options(url, None, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db +@pytest.mark.license_feature def test_no_facts_db(hosts, get, user): hosts = hosts(host_count=1) url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) @@ -72,28 +100,19 @@ def test_basic_fields(hosts, fact_scans, get, user): @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db -@pytest.mark.skipif(True, reason="Options fix landed in devel but not here. Enable this after this pr gets merged.") +@pytest.mark.license_feature def test_basic_options_fields(hosts, fact_scans, options, user): hosts = hosts(host_count=1) fact_scans(fact_scans=1) url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) - response = options(url, user('admin', True), pk=hosts[0].id) + response = options(url, None, user('admin', True), pk=hosts[0].id) - #import json - #print(json.dumps(response.data)) - assert 'related' in response.data - assert 'id' in response.data - assert 'facts' in response.data - assert 'module' in response.data - assert 'host' in response.data - assert isinstance(response.data['host'], int) - assert 'summary_fields' in response.data - assert 'host' in response.data['summary_fields'] - assert 'name' in response.data['summary_fields']['host'] - assert 'description' in response.data['summary_fields']['host'] - assert 'host' in response.data['related'] - assert reverse('api:host_detail', args=(hosts[0].pk,)) == response.data['related']['host'] + assert 'related' in response.data['actions']['GET'] + assert 'module' in response.data['actions']['GET'] + assert ("ansible", "Ansible") in response.data['actions']['GET']['module']['choices'] + assert ("services", "Services") in response.data['actions']['GET']['module']['choices'] + assert ("packages", "Packages") in response.data['actions']['GET']['module']['choices'] @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db diff --git a/awx/main/tests/functional/api/test_fact_view.py b/awx/main/tests/functional/api/test_fact_view.py index e6cd724d91..ad96d48aee 100644 --- a/awx/main/tests/functional/api/test_fact_view.py +++ b/awx/main/tests/functional/api/test_fact_view.py @@ -1,7 +1,6 @@ import mock import pytest import json -import urllib from awx.main.utils import timestamp_apiformat from django.core.urlresolvers import reverse @@ -10,6 +9,9 @@ from django.utils import timezone def mock_feature_enabled(feature, bypass_database=None): return True +def mock_feature_disabled(feature, bypass_database=None): + return False + # TODO: Consider making the fact_scan() fixture a Class, instead of a function, and move this method into it def find_fact(facts, host_id, module_name, timestamp): for f in facts: @@ -27,6 +29,30 @@ def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), module_name fact_known = find_fact(facts, hosts[0].id, module_name, epoch) return (fact_known, response) +def check_system_tracking_feature_forbidden(response): + assert 402 == response.status_code + assert 'Your license does not permit use of system tracking.' == response.data['detail'] + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_get(hosts, get, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,)) + response = get(url, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_options(hosts, options, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,)) + response = options(url, None, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_no_fact_found(hosts, get, user): diff --git a/awx/main/tests/functional/commands/test_cleanup_facts.py b/awx/main/tests/functional/commands/test_cleanup_facts.py new file mode 100644 index 0000000000..93ddb72d14 --- /dev/null +++ b/awx/main/tests/functional/commands/test_cleanup_facts.py @@ -0,0 +1,200 @@ +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved + +# Python +import pytest +import mock +from dateutil.relativedelta import relativedelta +from datetime import timedelta + +# Django +from django.utils import timezone +from django.core.management.base import CommandError + +# AWX +from awx.main.management.commands.cleanup_facts import CleanupFacts, Command +from awx.main.models.fact import Fact +from awx.main.models.inventory import Host + +def mock_feature_enabled(feature, bypass_database=None): + return True + +def mock_feature_disabled(feature, bypass_database=None): + return False + +@pytest.mark.django_db +def test_cleanup_granularity(fact_scans, hosts): + epoch = timezone.now() + hosts(5) + fact_scans(10, timestamp_epoch=epoch) + fact_newest = Fact.objects.all().order_by('-timestamp').first() + timestamp_future = fact_newest.timestamp + timedelta(days=365) + granularity = relativedelta(days=2) + + cleanup_facts = CleanupFacts() + deleted_count = cleanup_facts.cleanup(timestamp_future, granularity) + assert 60 == deleted_count + +''' +Delete half of the scans +''' +@pytest.mark.django_db +def test_cleanup_older_than(fact_scans, hosts): + epoch = timezone.now() + hosts(5) + fact_scans(28, timestamp_epoch=epoch) + qs = Fact.objects.all().order_by('-timestamp') + fact_middle = qs[qs.count() / 2] + granularity = relativedelta() + + cleanup_facts = CleanupFacts() + deleted_count = cleanup_facts.cleanup(fact_middle.timestamp, granularity) + assert 210 == deleted_count + +@pytest.mark.django_db +def test_cleanup_older_than_granularity_module(fact_scans, hosts): + epoch = timezone.now() + hosts(5) + fact_scans(10, timestamp_epoch=epoch) + fact_newest = Fact.objects.all().order_by('-timestamp').first() + timestamp_future = fact_newest.timestamp + timedelta(days=365) + granularity = relativedelta(days=2) + + cleanup_facts = CleanupFacts() + deleted_count = cleanup_facts.cleanup(timestamp_future, granularity, module='ansible') + assert 20 == deleted_count + + +''' +Reduce the granularity of half of the facts scans, by half. +''' +@pytest.mark.django_db +def test_cleanup_logic(fact_scans, hosts): + epoch = timezone.now() + hosts = hosts(5) + fact_scans(60, timestamp_epoch=epoch) + timestamp_middle = epoch + timedelta(days=30) + granularity = relativedelta(days=2) + module = 'ansible' + + cleanup_facts = CleanupFacts() + cleanup_facts.cleanup(timestamp_middle, granularity, module=module) + + + host_ids = Host.objects.all().values_list('id', flat=True) + host_facts = {} + for host_id in host_ids: + facts = Fact.objects.filter(host__id=host_id, module=module, timestamp__lt=timestamp_middle).order_by('-timestamp') + host_facts[host_id] = facts + + for host_id, facts in host_facts.iteritems(): + assert 15 == len(facts) + + timestamp_pivot = timestamp_middle + for fact in facts: + timestamp_pivot -= granularity + assert fact.timestamp == timestamp_pivot + +@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_feature_disabled(mocker): + cmd = Command() + with pytest.raises(CommandError) as err: + cmd.handle(None) + assert 'The System Tracking feature is not enabled for your Tower instance' in err.value + +@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled) +@pytest.mark.django_db +def test_parameters_ok(mocker): + run = mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run') + kv = { + 'older_than': '1d', + 'granularity': '1d', + 'module': None, + } + cmd = Command() + cmd.handle(None, **kv) + run.assert_called_once_with(relativedelta(days=1), relativedelta(days=1), module=None) + +@pytest.mark.django_db +def test_string_time_to_timestamp_ok(): + kvs = [ + { + 'time': '2w', + 'timestamp': relativedelta(weeks=2), + 'msg': '2 weeks', + }, + { + 'time': '23d', + 'timestamp': relativedelta(days=23), + 'msg': '23 days', + }, + { + 'time': '11m', + 'timestamp': relativedelta(months=11), + 'msg': '11 months', + }, + { + 'time': '14y', + 'timestamp': relativedelta(years=14), + 'msg': '14 years', + }, + ] + for kv in kvs: + cmd = Command() + res = cmd.string_time_to_timestamp(kv['time']) + assert kv['timestamp'] == res + +@pytest.mark.django_db +def test_string_time_to_timestamp_invalid(): + kvs = [ + { + 'time': '2weeks', + 'msg': 'weeks instead of w', + }, + { + 'time': '2days', + 'msg': 'days instead of d', + }, + { + 'time': '23', + 'msg': 'no unit specified', + }, + { + 'time': None, + 'msg': 'no value specified', + }, + { + 'time': 'zigzag', + 'msg': 'random string specified', + }, + ] + for kv in kvs: + cmd = Command() + res = cmd.string_time_to_timestamp(kv['time']) + assert res is None + +@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled) +@pytest.mark.django_db +def test_parameters_fail(mocker): + # Mock run() just in case, but it should never get called because an error should be thrown + mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run') + kvs = [ + { + 'older_than': '1week', + 'granularity': '1d', + 'msg': '--older_than invalid value "1week"', + }, + { + 'older_than': '1d', + 'granularity': '1year', + 'msg': '--granularity invalid value "1year"', + } + ] + for kv in kvs: + cmd = Command() + with pytest.raises(CommandError) as err: + cmd.handle(None, older_than=kv['older_than'], granularity=kv['granularity']) + assert kv['msg'] in err.value + diff --git a/awx/main/tests/old/commands/cleanup_facts.py b/awx/main/tests/old/commands/cleanup_facts.py deleted file mode 100644 index fc0f049aad..0000000000 --- a/awx/main/tests/old/commands/cleanup_facts.py +++ /dev/null @@ -1,238 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved - -# Python -from datetime import datetime -from dateutil.relativedelta import relativedelta -import mock - -#Django -from django.core.management.base import CommandError - -# AWX -from awx.main.tests.base import BaseTest -from awx.fact.tests.base import MongoDBRequired, FactScanBuilder, TEST_FACT_PACKAGES, TEST_FACT_ANSIBLE, TEST_FACT_SERVICES -from command_base import BaseCommandMixin -from awx.main.management.commands.cleanup_facts import Command, CleanupFacts -from awx.fact.models.fact import * # noqa - -__all__ = ['CommandTest','CleanupFactsUnitTest', 'CleanupFactsCommandFunctionalTest'] - -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) - - def test_invoke_zero_ok(self): - self.builder.set_epoch(datetime(year=2015, day=2, month=1, microsecond=0)) - self.builder.build(scan_count=20, host_count=10) - - result, stdout, stderr = self.run_command('cleanup_facts', granularity='2y', older_than='1d') - self.assertEqual(stdout, 'Deleted %s facts.\n' % ((200 / 2))) - - def test_invoke_zero_deleted(self): - result, stdout, stderr = self.run_command('cleanup_facts', granularity='1w',older_than='5d') - self.assertEqual(stdout, 'Deleted 0 facts.\n') - - def test_invoke_all_deleted(self): - self.builder.build(scan_count=20, host_count=10) - - result, stdout, stderr = self.run_command('cleanup_facts', granularity='0d', older_than='0d') - self.assertEqual(stdout, 'Deleted 200 facts.\n') - - def test_invoke_params_required(self): - result, stdout, stderr = self.run_command('cleanup_facts') - self.assertIsInstance(result, CommandError) - self.assertEqual(str(result), 'Both --granularity and --older_than are required.') - - def test_module(self): - self.builder.add_fact('packages', TEST_FACT_PACKAGES) - self.builder.add_fact('services', TEST_FACT_SERVICES) - self.builder.build(scan_count=5, host_count=5) - - result, stdout, stderr = self.run_command('cleanup_facts', granularity='0d', older_than='0d', module='packages') - 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): - - kv = { - 'older_than': '1d', - 'granularity': '1d', - 'module': None, - } - cmd = Command() - cmd.handle(None, **kv) - run.assert_called_once_with(relativedelta(days=1), relativedelta(days=1), module=None) - - def test_string_time_to_timestamp_ok(self): - kvs = [ - { - 'time': '2w', - 'timestamp': relativedelta(weeks=2), - 'msg': '2 weeks', - }, - { - 'time': '23d', - 'timestamp': relativedelta(days=23), - 'msg': '23 days', - }, - { - 'time': '11m', - 'timestamp': relativedelta(months=11), - 'msg': '11 months', - }, - { - 'time': '14y', - 'timestamp': relativedelta(years=14), - 'msg': '14 years', - }, - ] - for kv in kvs: - cmd = Command() - res = cmd.string_time_to_timestamp(kv['time']) - self.assertEqual(kv['timestamp'], res, "%s should convert to %s" % (kv['time'], kv['msg'])) - - def test_string_time_to_timestamp_invalid(self): - kvs = [ - { - 'time': '2weeks', - 'msg': 'weeks instead of w', - }, - { - 'time': '2days', - 'msg': 'days instead of d', - }, - { - 'time': '23', - 'msg': 'no unit specified', - }, - { - 'time': None, - 'msg': 'no value specified', - }, - { - 'time': 'zigzag', - 'msg': 'random string specified', - }, - ] - for kv in kvs: - cmd = Command() - res = cmd.string_time_to_timestamp(kv['time']) - self.assertIsNone(res, kv['msg']) - - # Mock run() just in case, but it should never get called because an error should be thrown - @mock.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run') - def test_parameters_fail(self, run): - kvs = [ - { - 'older_than': '1week', - 'granularity': '1d', - 'msg': 'Invalid older_than param value', - }, - { - 'older_than': '1d', - 'granularity': '1year', - 'msg': 'Invalid granularity param value', - } - ] - for kv in kvs: - cmd = Command() - with self.assertRaises(CommandError): - cmd.handle(None, older_than=kv['older_than'], granularity=kv['granularity']) - -class CleanupFactsUnitTest(BaseCommandMixin, BaseTest, MongoDBRequired): - def setUp(self): - super(CleanupFactsUnitTest, self).setUp() - - 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 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): - 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) - 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() - scan_count = (2 * self.builder.get_scan_count()) / 2 - self.assertEqual(count, scan_count) - - count = Fact.objects.filter(host=host).count() - self.assertEqual(count, scan_count) - - # 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: - date_pivot_next = date_pivot - granularity - kv = { - 'timestamp__lte': date_pivot, - 'timestamp__gt': date_pivot_next, - 'host': host, - } - count = FactVersion.objects.filter(**kv).count() - self.assertEqual(count, 2, "should only be 2 FactVersion per the 2 year granularity") - count = Fact.objects.filter(**kv).count() - 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/client/assets/cowsay-about.html b/awx/ui/client/assets/cowsay-about.html deleted file mode 100644 index b1cbcf9d24..0000000000 --- a/awx/ui/client/assets/cowsay-about.html +++ /dev/null @@ -1,29 +0,0 @@ - -
- ________________ -/ Tower Version \ -\/ - ---------------- - \ ^__^ - \ (oo)\_______ - (__)\ A)\/\ - ||----w | - || || --

Copyright 2015. All rights reserved.
-Ansible and Ansible Tower are registered trademarks of Red Hat, Inc.
-
- Visit Ansible.com for more information.
--
'+x+'
'+ '' + Math.floor(y.replace(',','')) + ' HOSTS ' + '
'; + }) + .color(colors); - arc = d3.svg.arc() - .innerRadius(outerRadius - innerRadius) - .outerRadius(outerRadius); - - pie = d3.layout.pie() - .value(function(d) { return d.value; }) - .sort(function() {return null; }); - - tooltip = d3.select(target) - .append('div') - .attr('class', 'donut-tooltip'); - - tooltip.append('div') - .attr('class', 'donut-tooltip-inner'); - - path = svg.selectAll('path') - .data(pie(dataset)) - .enter() - .append('path') - .attr('d', arc) - .attr('fill', function(d) { - return d.data.color; + d3.select(element.find('svg')[0]) + .datum(dataset) + .transition().duration(350) + .call(job_detail_chart) + .style({ + "font-family": 'Open Sans', + "font-style": "normal", + "font-weight":400, + "src": "url(/static/assets/OpenSans-Regular.ttf)" }); - path.on('mouseenter', function(d) { - var total = d3.sum(dataset.map(function(d) { - return d.value; - })); - - var label; - if (d.data.value === 1) { - label = " host "; - } else { - label = " hosts "; - } - var percent = Math.round(1000 * d.data.value / total) / 10; - tooltip.select('.donut-tooltip-inner').html(d.data.value + label + " (" + - percent + "%) " + d.data.label + "."); - //.attr('style', 'color:white;font-family:'); - tooltip.style('display', 'block'); - }); - - path.on('mouseleave', function() { - tooltip.style('display', 'none'); - }); - - path.on('mousemove', function() { - // d3.mouse() gives the coordinates of hte mouse, then add - // some offset to provide breathing room for hte tooltip - // based on the dimensions of the donut - tooltip.style('top', (d3.mouse(this)[1] + (height/5) + 'px')) - .style('left', (d3.mouse(this)[0] + (width/3) + 'px')); - }); - - legend = svg.selectAll('.legend') - .data(pie(dataset)) - .enter() - .append('g') - .attr('class', 'legend') - .attr('transform', function(d, i) { - var height = legendRectSize + legendSpacing; - var offset = height * dataset.length / 2; - var horz = -2 * legendRectSize; - var vert = i * height - offset; - return 'translate(' + horz + ',' + vert + ')'; - }); - - legend.append('rect') - .attr('width', legendRectSize) - .attr('height', legendRectSize) - .attr('fill', function(d) { - return d.data.color; - }) - .attr('stroke', function(d) { - return d.data.color; - }); - - legend.append('text') - .attr('x', legendRectSize + legendSpacing) - .attr('y', legendRectSize - legendSpacing) - .text(function(d) { - return d.data.label; - }); + d3.select(element.find(".nv-label text")[0]) + .attr("class", "DashboardGraphs-hostStatusLabel--successful") + .style({ + "font-family": 'Open Sans', + "text-anchor": "start", + "font-size": "16px", + "text-transform" : "uppercase", + "fill" : colors[0], + "src": "url(/static/assets/OpenSans-Regular.ttf)" + }); + d3.select(element.find(".nv-label text")[1]) + .attr("class", "DashboardGraphs-hostStatusLabel--failed") + .style({ + "font-family": 'Open Sans', + "text-anchor" : "end !imporant", + "font-size": "16px", + "text-transform" : "uppercase", + "fill" : colors[1], + "src": "url(/static/assets/OpenSans-Regular.ttf)" + }); + d3.select(element.find(".nv-label text")[2]) + .attr("class", "DashboardGraphs-hostStatusLabel--successful") + .style({ + "font-family": 'Open Sans', + "text-anchor" : "end !imporant", + "font-size": "16px", + "text-transform" : "uppercase", + "fill" : colors[2], + "src": "url(/static/assets/OpenSans-Regular.ttf)" + }); + d3.select(element.find(".nv-label text")[3]) + .attr("class", "DashboardGraphs-hostStatusLabel--failed") + .style({ + "font-family": 'Open Sans', + "text-anchor" : "end !imporant", + "font-size": "16px", + "text-transform" : "uppercase", + "fill" : colors[3], + "src": "url(/static/assets/OpenSans-Regular.ttf)" + }); + return job_detail_chart; }; }]) diff --git a/awx/ui/client/src/helpers/License.js b/awx/ui/client/src/helpers/License.js deleted file mode 100644 index 70cbb59c72..0000000000 --- a/awx/ui/client/src/helpers/License.js +++ /dev/null @@ -1,271 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - /** - * @ngdoc function - * @name helpers.function:License - * @description Routines for checking and reporting license status - * CheckLicense.test() is called in app.js, in line 532, which is when the license is checked. The license information is - * stored in local storage using 'Store()'. - * - * - * - * -*/ - -import '../forms'; - -export default - angular.module('LicenseHelper', ['RestServices', 'Utilities', 'LicenseUpdateFormDefinition', - 'FormGenerator', 'ParseHelper', 'ModalDialog', 'VariablesHelper', 'LicenseFormDefinition']) - - - .factory('CheckLicense', ['$q', '$rootScope', '$compile', 'CreateDialog', 'Store', - 'LicenseUpdateForm', 'GenerateForm', 'TextareaResize', 'ToJSON', 'GetBasePath', - 'Rest', 'ProcessErrors', 'Alert', 'IsAdmin', '$location', 'pendoService', - 'Authorization', 'Wait', - function($q, $rootScope, $compile, CreateDialog, Store, LicenseUpdateForm, GenerateForm, - TextareaResize, ToJSON, GetBasePath, Rest, ProcessErrors, Alert, IsAdmin, $location, - pendoService, Authorization, Wait) { - return { - getRemainingDays: function(time_remaining) { - // assumes time_remaining will be in seconds - var tr = parseInt(time_remaining, 10); - return Math.floor(tr / 86400); - }, - - shouldNotify: function(license) { - if (license && typeof license === 'object' && Object.keys(license).length > 0) { - // we have a license object - if (!license.valid_key) { - // missing valid key - return true; - } - else if (license.free_instances <= 0) { - // host count exceeded - return true; - } - else if (this.getRemainingDays(license.time_remaining) < 15) { - // below 15 days remaining on license - return true; - } - return false; - } else { - // missing license object - return true; - } - }, - - isAdmin: function() { - return IsAdmin(); - }, - - getHTML: function(license, includeFormButton) { - - var title, html, - contact_us = "contact us ", - renew = "ansible.com/renew ", - pricing = "ansible.com/pricing ", - license_link = "click here", - result = {}, - license_is_valid=false; - - if (license && typeof license === 'object' && Object.keys(license).length > 0 && license.valid_key !== undefined) { - // we have a license - if (!license.valid_key) { - title = "Invalid License"; - html = "The Ansible Tower license is invalid.
"; - } - else if (this.getRemainingDays(license.time_remaining) <= 0) { - title = "License Expired"; - html = "Thank you for using Ansible Tower. The Ansible Tower license has expired
"; - if (parseInt(license.grace_period_remaining,10) > 86400) { - // trial licenses don't get a grace period - if (license.trial) { - html += "Don't worry — your existing history and content has not been affected, but playbooks will no longer run and new hosts cannot be added. " + - "If you are ready to upgrade, " + contact_us + " or visit " + pricing + " to see all of your license options. Thanks!
"; - } else { - html += "Don't worry — your existing history and content has not been affected, but in " + this.getRemainingDays(license.grace_period_remaining) + " days playbooks will no longer " + - "run and new hosts cannot be added. If you are ready to upgrade, " + contact_us + " " + - "or visit ansible.com/pricing to see all of your license options. Thanks!
"; - } - } else { - html += "Don’t worry — your existing history and content has not been affected, but playbooks will no longer run and new hosts cannot be added. If you are ready to renew or upgrade, contact us " + - "at " + renew + ". Thanks!
"; - } - } - else if (this.getRemainingDays(license.time_remaining) < 15) { - // Warning: license expiring in less than 15 days - title = "License Warning"; - html = "Thank you for using Ansible Tower. The Ansible Tower license " + - "has " + this.getRemainingDays(license.time_remaining) + " days remaining.
"; - // trial licenses don't get a grace period - if (license.trial) { - html += "After this license expires, playbooks will no longer run and hosts cannot be added. If you are ready to upgrade, " + contact_us + " or visit " + pricing + " to see all of your license options. Thanks!
"; - } else { - html += "After this license expires, playbooks will no longer run and hosts cannot be added. If you are ready to renew or upgrade, contact us at " + renew + ". Thanks!
"; - } - - // If there is exactly one day remaining, change "days remaining" - // to "day remaining". - html = html.replace('has 1 days remaining', 'has 1 day remaining'); - } - else if (license.free_instances <= 0) { - title = "Host Count Exceeded"; - html = "The Ansible Tower license has reached capacity for the number of managed hosts allowed. No new hosts can be added. Existing " + - "playbooks can still be run against hosts already in inventory.
" + - "If you are ready to upgrade, contact us at " + renew + ". Thanks!
"; - - } else { - // license is valid. the following text is displayed in the license viewer - title = "Update License"; - html = "The Ansible Tower license is valid.
" + - "If you are ready to upgrade, contact us at " + renew + ". Thanks!
"; - license_is_valid = true; - } - } else { - // No license - title = "Add Your License"; - html = "Now that you’ve successfully installed or upgraded Ansible Tower, the next step is to add a license file. " + - "If you don’t have a license file yet, " + license_link + " to see all of our free and paid license options.
" + - "Get a Free Tower Trial License
"; - } - - if (IsAdmin()) { - html += "Copy and paste the contents of your license in the field below, agree to the End User License Agreement, and click Submit.
"; - } else { - html += "A system administrator can install the new license by choosing View License on the Account Menu and clicking on the Update License tab.
"; - } - - html += "| Started | -Elapsed | -Status | -Name | -
|---|
| {{ play.created | date: 'HH:mm:ss' }} | -{{ play.elapsed }} | -- | {{ play.name }} | -
| Waiting... | -|||
| Loading... | -|||
| No matching plays | -|||
| Started | -Elapsed | -Status | -Name - | -
|---|
| {{ task.created | date: 'HH:mm:ss' }} | -{{ task.elapsed }} | -- | {{ task.name }} | - -|
| Waiting... | -||||
| Loading... | -||||
| No matching tasks | -||||
| Status | -Host | -Item | -Message | -- |
|---|
| - | {{ result.name }} | -{{ result.item }} | -{{ result.msg }} | -- |
| Waiting... | -||||
| Loading... | -||||
| No matching host events | -||||
| Plays | +Started | +Elapsed | +
|---|
| {{ play.name }} | +{{ play.created | date: 'HH:mm:ss' }} | +{{ play.elapsed }} | + +|
| Waiting... | +|||
| Loading... | +|||
| No matching plays | +|||
| Tasks | +Started | +Elapsed | + +
|---|
| {{ task.name }} | +{{ task.created | date: 'HH:mm:ss' }} | +{{ task.elapsed }} | + + +||
| Waiting... | +||||
| Loading... | +||||
| No matching tasks | +||||
| Hosts | +Item | +Message | +
|---|
| {{ result.name }}{{ result.name }} | +{{ result.item }} | +{{ result.msg }} | +||
| Waiting... | +||||
| Loading... | +||||
| No matching host events | +||||