diff --git a/ansibleworks/main/management/__init__.py b/ansibleworks/main/management/__init__.py deleted file mode 100644 index bfec7ec6ea..0000000000 --- a/ansibleworks/main/management/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2013 AnsibleWorks, Inc. -# All Rights Reserved. diff --git a/ansibleworks/main/management/commands/__init__.py b/ansibleworks/main/management/commands/__init__.py deleted file mode 100644 index edb7cdf758..0000000000 --- a/ansibleworks/main/management/commands/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2013 AnsibleWorks, Inc. -# All Rights Reserved. - -import os -import sys - -def run_command_as_script(command_name): - ''' - Helper function to run the given management command directly as a script. - - Include something like the following in your management/commands/blah.py: - - if __name__ == '__main__': - from __init__ import run_command_as_script - command_name = os.path.splitext(os.path.basename(__file__))[0] - run_command_as_script(command_name) - - ''' - # The DJANGO_SETTINGS_MODULE environment variable should already be set if - # the script is called from a celery task. Don't attemtp to set a default. - settings_module_name = os.environ['DJANGO_SETTINGS_MODULE'] - # This sys.path hack is needed when a celery task calls ansible-playbook - # and needs to execute the script directly. FIXME: Figure out if this will - # work when installed in a production environment. - try: - settings_module = __import__(settings_module_name, globals(), locals(), - [settings_module_name.split('.')[-1]]) - except ImportError: - top_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', '..') - sys.path.insert(0, os.path.abspath(top_dir)) - settings_module = __import__(settings_module_name, globals(), locals(), - [settings_module_name.split('.')[-1]]) - # Use the ACOM_TEST_DATABASE_NAME environment variable to specify the test - # database name when called from unit tests. - if os.environ.get('ACOM_TEST_DATABASE_NAME', None): - settings_module.DATABASES['default']['NAME'] = os.environ['ACOM_TEST_DATABASE_NAME'] - from django.core.management import execute_from_command_line - argv = [sys.argv[0], command_name] + sys.argv[1:] - execute_from_command_line(argv) diff --git a/ansibleworks/main/management/commands/acom_callback_event.py b/ansibleworks/main/management/commands/acom_callback_event.py deleted file mode 100755 index a228dea27c..0000000000 --- a/ansibleworks/main/management/commands/acom_callback_event.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2013 AnsibleWorks, Inc. -# All Rights Reserved. - -import json -from optparse import make_option -import os -import sys -from django.core.management.base import NoArgsCommand, CommandError -from django.db import transaction - -class Command(NoArgsCommand): - ''' - Management command to log callback events from ansible-playbook. - ''' - - help = 'Ansible Commander Callback Event Capture' - - option_list = NoArgsCommand.option_list + ( - make_option('-j', '--job', dest='job_id', - type='int', default=0, - help='Job ID (can also be specified using ACOM_JOB_ID ' - 'environment variable)'), - make_option('-e', '--event', dest='event_type', default=None, - help='Event type'), - make_option('-f', '--file', dest='event_data_file', default=None, - help='JSON-formatted data file containing callback event ' - 'data (specify "-" to read from stdin)'), - make_option('-d', '--data', dest='event_data_json', default=None, - help='JSON-formatted callback event data'), - ) - - @transaction.commit_on_success - def handle_noargs(self, **options): - from ansibleworks.main.models import Job, JobEvent - event_type = options.get('event_type', None) - if not event_type: - raise CommandError('No event specified') - if event_type not in [x[0] for x in JobEvent.EVENT_CHOICES]: - raise CommandError('Unsupported event') - event_data_file = options.get('event_data_file', None) - event_data_json = options.get('event_data_json', None) - if event_data_file is None and event_data_json is None: - raise CommandError('Either --file or --data must be specified') - try: - job_id = int(os.getenv('ACOM_JOB_ID', options.get('job_id', 0))) - except ValueError: - raise CommandError('Job ID must be an integer') - if not job_id: - raise CommandError('No Job ID specified') - try: - job = Job.objects.get(id=job_id) - except Job.DoesNotExist: - raise CommandError('Job with ID %d not found' % job_id) - if job.status != 'running': - raise CommandError('Unable to add event except when job is running' - ', status is currently %s' % job.status) - try: - if event_data_json is None: - try: - if event_data_file == '-': - event_data_fileobj = sys.stdin - else: - event_data_fileobj = file(event_data_file, 'rb') - event_data = json.load(event_data_fileobj) - except IOError, e: - raise CommandError('Error %r reading from %s' % (e, event_data_file)) - else: - event_data = json.loads(event_data_json) - except ValueError: - raise CommandError('Error parsing JSON data') - job.job_events.create(event=event_type, event_data=event_data) - -if __name__ == '__main__': - from __init__ import run_command_as_script - command_name = os.path.splitext(os.path.basename(__file__))[0] - run_command_as_script(command_name) diff --git a/ansibleworks/main/management/commands/acom_inventory.py b/ansibleworks/main/management/commands/acom_inventory.py deleted file mode 100755 index 5cb2b52b94..0000000000 --- a/ansibleworks/main/management/commands/acom_inventory.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2013 AnsibleWorks, Inc. -# All Rights Reserved. - -import json -from optparse import make_option -import os -from django.core.management.base import NoArgsCommand, CommandError - -class Command(NoArgsCommand): - - help = 'Ansible Commander Inventory script' - - option_list = NoArgsCommand.option_list + ( - make_option('-i', '--inventory', dest='inventory_id', type='int', - default=0, help='Inventory ID (can also be specified using' - ' ACOM_INVENTORY_ID environment variable)'), - make_option('--list', action='store_true', dest='list', default=False, - help='Return JSON hash of host groups.'), - make_option('--host', dest='host', default='', - help='Return JSON hash of host vars.'), - make_option('--indent', dest='indent', type='int', default=None, - help='Indentation level for pretty printing output'), - ) - - def get_list(self, inventory, indent=None): - groups = {} - for group in inventory.groups.filter(active=True): - hosts = group.hosts.filter(active=True) - children = group.children.filter(active=True) - group_info = { - 'hosts': list(hosts.values_list('name', flat=True)), - 'children': list(children.values_list('name', flat=True)), - 'vars': group.variables_dict, - } - group_info = dict(filter(lambda x: bool(x[1]), group_info.items())) - if group_info.keys() in ([], ['hosts']): - groups[group.name] = group_info.get('hosts', []) - else: - groups[group.name] = group_info - if inventory.variables_dict: - groups['all'] = { - 'vars': inventory.variables_dict, - } - self.stdout.write(json.dumps(groups, indent=indent)) - - def get_host(self, inventory, hostname, indent=None): - from ansibleworks.main.models import Host - hostvars = {} - try: - host = inventory.hosts.get(active=True, name=hostname) - except Host.DoesNotExist: - raise CommandError('Host %s not found in the given inventory' % hostname) - hostvars = {} - if host.variables: - hostvars = host.variables_dict - self.stdout.write(json.dumps(hostvars, indent=indent)) - - def handle_noargs(self, **options): - try: - from ansibleworks.main.models import Inventory - try: - # Command line argument takes precedence over environment - # variable. - inventory_id = int(options.get('inventory_id', 0) or \ - os.getenv('ACOM_INVENTORY_ID', 0)) - except ValueError: - raise CommandError('Inventory ID must be an integer') - if not inventory_id: - raise CommandError('No inventory ID specified') - try: - inventory = Inventory.objects.get(active=True, id=inventory_id) - except Inventory.DoesNotExist: - raise CommandError('Inventory with ID %d not found' % inventory_id) - host = options.get('host', '') - list_ = options.get('list', False) - indent = options.get('indent', None) - if list_ and host: - raise CommandError('Only one of --list or --host can be specified') - elif list_: - self.get_list(inventory, indent=indent) - elif host: - self.get_host(inventory, host, indent=indent) - else: - raise CommandError('Either --list or --host must be specified') - except CommandError, e: - # Always return an empty hash on stdout, even when an error occurs. - self.stdout.write(json.dumps({})) - raise - -if __name__ == '__main__': - from __init__ import run_command_as_script - command_name = os.path.splitext(os.path.basename(__file__))[0] - run_command_as_script(command_name) diff --git a/ansibleworks/main/rbac.py b/ansibleworks/main/rbac.py index 242c89f20b..afa41b41b4 100644 --- a/ansibleworks/main/rbac.py +++ b/ansibleworks/main/rbac.py @@ -6,6 +6,7 @@ from django.http import Http404 from rest_framework.exceptions import PermissionDenied from rest_framework import permissions from ansibleworks.main.access import * +from ansibleworks.main.models import * logger = logging.getLogger('ansibleworks.main.rbac') @@ -126,5 +127,13 @@ class JobCallbackPermission(CustomRbac): return super(JobCallbackPermission, self).has_permission(request, view, obj) # FIXME: Verify that inventory or job event requested are for the same # job ID present in the auth token, etc. - return True - + #try: + # job = Job.objects.get(active=True, status='running', pk=int(request.auth.split('-')[0])) + #except Job.DoesNotExist: + # return False + if view.model == Inventory and request.method.lower() in ('head', 'get'): + return True + elif view.model == JobEvent and request.method.lower() == 'post': + return True + else: + return False diff --git a/ansibleworks/main/tasks.py b/ansibleworks/main/tasks.py index 412cb49c57..8d5aff34ed 100644 --- a/ansibleworks/main/tasks.py +++ b/ansibleworks/main/tasks.py @@ -79,16 +79,12 @@ class RunJob(Task): Build environment dictionary for ansible-playbook. ''' plugin_dir = self.get_path_to('..', 'plugins', 'callback') - callback_script = self.get_path_to('management', 'commands', - 'acom_callback_event.py') env = dict(os.environ.items()) # question: when running over CLI, generate a random ID or grab next, etc? # answer: TBD - env['ACOM_JOB_ID'] = str(job.pk) - env['ACOM_INVENTORY_ID'] = str(job.inventory.pk) + env['JOB_ID'] = str(job.pk) env['INVENTORY_ID'] = str(job.inventory.pk) env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir - env['ACOM_CALLBACK_EVENT_SCRIPT'] = callback_script if hasattr(settings, 'ANSIBLE_TRANSPORT'): env['ANSIBLE_TRANSPORT'] = getattr(settings, 'ANSIBLE_TRANSPORT') env['REST_API_URL'] = settings.INTERNAL_API_URL diff --git a/ansibleworks/main/tests/__init__.py b/ansibleworks/main/tests/__init__.py index 6571457980..eb51ca54c9 100644 --- a/ansibleworks/main/tests/__init__.py +++ b/ansibleworks/main/tests/__init__.py @@ -5,7 +5,6 @@ from ansibleworks.main.tests.organizations import OrganizationsTest from ansibleworks.main.tests.users import UsersTest from ansibleworks.main.tests.inventory import InventoryTest from ansibleworks.main.tests.projects import ProjectsTest -from ansibleworks.main.tests.commands import * from ansibleworks.main.tests.scripts import * from ansibleworks.main.tests.tasks import RunJobTest from ansibleworks.main.tests.jobs import * diff --git a/ansibleworks/main/tests/commands.py b/ansibleworks/main/tests/commands.py deleted file mode 100644 index 5572fa3d2a..0000000000 --- a/ansibleworks/main/tests/commands.py +++ /dev/null @@ -1,441 +0,0 @@ -# Copyright (c) 2013 AnsibleWorks, Inc. -# All Rights Reserved. - -import json -import os -import StringIO -import sys -import tempfile -from django.conf import settings -from django.core.management import call_command -from django.core.management.base import CommandError -from django.utils.timezone import now -from ansibleworks.main.models import * -from ansibleworks.main.tests.base import BaseTest - -__all__ = ['RunCommandAsScriptTest',# 'AcomInventoryTest', - 'AcomCallbackEventTest'] - -class BaseCommandTest(BaseTest): - ''' - Base class for tests that run management commands. - ''' - - def setUp(self): - super(BaseCommandTest, self).setUp() - self._sys_path = [x for x in sys.path] - self._environ = dict(os.environ.items()) - self._temp_files = [] - - def tearDown(self): - super(BaseCommandTest, self).tearDown() - sys.path = self._sys_path - for k,v in self._environ.items(): - if os.environ.get(k, None) != v: - os.environ[k] = v - for k,v in os.environ.items(): - if k not in self._environ.keys(): - del os.environ[k] - for tf in self._temp_files: - if os.path.exists(tf): - os.remove(tf) - - def run_command(self, name, *args, **options): - ''' - Run a management command and capture its stdout/stderr along with any - exceptions. - ''' - command_runner = options.pop('command_runner', call_command) - stdin_fileobj = options.pop('stdin_fileobj', None) - options.setdefault('verbosity', 1) - options.setdefault('interactive', False) - original_stdin = sys.stdin - original_stdout = sys.stdout - original_stderr = sys.stderr - if stdin_fileobj: - sys.stdin = stdin_fileobj - sys.stdout = StringIO.StringIO() - sys.stderr = StringIO.StringIO() - result = None - try: - result = command_runner(name, *args, **options) - except Exception, e: - result = e - except SystemExit, e: - result = e - finally: - captured_stdout = sys.stdout.getvalue() - captured_stderr = sys.stderr.getvalue() - sys.stdin = original_stdin - sys.stdout = original_stdout - sys.stderr = original_stderr - return result, captured_stdout, captured_stderr - -class RunCommandAsScriptTest(BaseCommandTest): - ''' - Test helper to run management command as standalone script. - ''' - - def test_run_command_as_script(self): - from ansibleworks.main.management.commands import run_command_as_script - os.environ['ACOM_TEST_DATABASE_NAME'] = settings.DATABASES['default']['NAME'] - # FIXME: Not sure how to test ImportError for settings module. - def run_cmd(name, *args, **kwargs): - return run_command_as_script(name) - result, stdout, stderr = self.run_command('version', - command_runner=run_cmd) - self.assertEqual(result, None) - self.assertTrue(stdout) - self.assertFalse(stderr) - -class AcomInventoryTest(BaseCommandTest): - ''' - Test cases for acom_inventory management command. - ''' - - def setUp(self): - super(AcomInventoryTest, self).setUp() - self.setup_users() - self.organizations = self.make_organizations(self.super_django_user, 2) - self.projects = self.make_projects(self.normal_django_user, 2) - self.organizations[0].projects.add(self.projects[1]) - self.organizations[1].projects.add(self.projects[0]) - self.inventories = [] - self.hosts = [] - self.groups = [] - for n, organization in enumerate(self.organizations): - inventory = Inventory.objects.create(name='inventory-%d' % n, - description='description for inventory %d' % n, - organization=organization, - variables=json.dumps({'n': n}) if n else '') - self.inventories.append(inventory) - hosts = [] - for x in xrange(10): - if n > 0: - variables = json.dumps({'ho': 'hum-%d' % x}) - else: - variables = '' - host = inventory.hosts.create(name='host-%02d-%02d.example.com' % (n, x), - inventory=inventory, - variables=variables) - if x in (3, 7): - host.mark_inactive() - hosts.append(host) - self.hosts.extend(hosts) - groups = [] - for x in xrange(5): - if n > 0: - variables = json.dumps({'gee': 'whiz-%d' % x}) - else: - variables = '' - group = inventory.groups.create(name='group-%d' % x, - inventory=inventory, - variables=variables) - if x == 2: - group.mark_inactive() - groups.append(group) - group.hosts.add(hosts[x]) - group.hosts.add(hosts[x + 5]) - if n > 0 and x == 4: - group.parents.add(groups[3]) - self.groups.extend(groups) - - def test_without_inventory_id(self): - result, stdout, stderr = self.run_command('acom_inventory', list=True) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - result, stdout, stderr = self.run_command('acom_inventory', - host=self.hosts[0]) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - - def test_list_with_inventory_id_as_argument(self): - inventory = self.inventories[0] - self.assertTrue(inventory.active) - result, stdout, stderr = self.run_command('acom_inventory', list=True, - inventory_id=inventory.pk) - self.assertEqual(result, None) - data = json.loads(stdout) - groups = inventory.groups.filter(active=True) - groupnames = groups.values_list('name', flat=True) - self.assertEqual(set(data.keys()), set(groupnames)) - # Groups for this inventory should only have hosts, and no group - # variable data or parent/child relationships. - for k,v in data.items(): - self.assertTrue(isinstance(v, (list, tuple))) - group = inventory.groups.get(active=True, name=k) - hosts = group.hosts.filter(active=True) - hostnames = hosts.values_list('name', flat=True) - self.assertEqual(set(v), set(hostnames)) - for group in inventory.groups.filter(active=False): - self.assertFalse(group.name in data.keys(), - 'deleted group %s should not be in data' % group) - # Command line argument for inventory ID should take precedence over - # environment variable. - inventory_pks = set(map(lambda x: x.pk, self.inventories)) - invalid_id = [x for x in xrange(9999) if x not in inventory_pks][0] - os.environ['ACOM_INVENTORY_ID'] = str(invalid_id) - result, stdout, stderr = self.run_command('acom_inventory', list=True, - inventory_id=inventory.pk) - self.assertEqual(result, None) - data = json.loads(stdout) - - def test_list_with_inventory_id_in_environment(self): - inventory = self.inventories[1] - self.assertTrue(inventory.active) - os.environ['ACOM_INVENTORY_ID'] = str(inventory.pk) - result, stdout, stderr = self.run_command('acom_inventory', list=True) - self.assertEqual(result, None) - data = json.loads(stdout) - groups = inventory.groups.filter(active=True) - groupnames = list(groups.values_list('name', flat=True)) + ['all'] - self.assertEqual(set(data.keys()), set(groupnames)) - # Groups for this inventory should have hosts, variable data, and one - # parent/child relationship. - for k,v in data.items(): - self.assertTrue(isinstance(v, dict)) - if k == 'all': - self.assertEqual(v.get('vars', {}), inventory.variables_dict) - continue - group = inventory.groups.get(active=True, name=k) - hosts = group.hosts.filter(active=True) - hostnames = hosts.values_list('name', flat=True) - self.assertEqual(set(v.get('hosts', [])), set(hostnames)) - if group.variables: - self.assertEqual(v.get('vars', {}), - json.loads(group.variables)) - if k == 'group-3': - children = group.children.filter(active=True) - childnames = children.values_list('name', flat=True) - self.assertEqual(set(v.get('children', [])), set(childnames)) - else: - self.assertFalse('children' in v) - - def test_valid_host(self): - # Host without variable data. - inventory = self.inventories[0] - self.assertTrue(inventory.active) - host = inventory.hosts.filter(active=True)[2] - os.environ['ACOM_INVENTORY_ID'] = str(inventory.pk) - result, stdout, stderr = self.run_command('acom_inventory', - host=host.name) - self.assertEqual(result, None) - data = json.loads(stdout) - self.assertEqual(data, {}) - # Host with variable data. - inventory = self.inventories[1] - self.assertTrue(inventory.active) - host = inventory.hosts.filter(active=True)[4] - os.environ['ACOM_INVENTORY_ID'] = str(inventory.pk) - result, stdout, stderr = self.run_command('acom_inventory', - host=host.name) - self.assertEqual(result, None) - data = json.loads(stdout) - self.assertEqual(data, json.loads(host.variables)) - - def test_invalid_host(self): - # Valid host, but not part of the specified inventory. - inventory = self.inventories[0] - self.assertTrue(inventory.active) - host = Host.objects.exclude(inventory=inventory)[0] - self.assertTrue(host.active) - os.environ['ACOM_INVENTORY_ID'] = str(inventory.pk) - result, stdout, stderr = self.run_command('acom_inventory', - host=host.name) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - # Invalid hostname not in database. - result, stdout, stderr = self.run_command('acom_inventory', - host='invalid.example.com') - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - - def test_with_invalid_inventory_id(self): - inventory_pks = set(map(lambda x: x.pk, self.inventories)) - invalid_id = [x for x in xrange(1, 9999) if x not in inventory_pks][0] - os.environ['ACOM_INVENTORY_ID'] = str(invalid_id) - result, stdout, stderr = self.run_command('acom_inventory', list=True) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - os.environ['ACOM_INVENTORY_ID'] = 'not_an_int' - result, stdout, stderr = self.run_command('acom_inventory', list=True) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - os.environ['ACOM_INVENTORY_ID'] = str(invalid_id) - result, stdout, stderr = self.run_command('acom_inventory', - host=self.hosts[1]) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - os.environ['ACOM_INVENTORY_ID'] = 'not_an_int' - result, stdout, stderr = self.run_command('acom_inventory', - host=self.hosts[2]) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - - def test_with_deleted_inventory(self): - inventory = self.inventories[0] - inventory.mark_inactive() - self.assertFalse(inventory.active) - os.environ['ACOM_INVENTORY_ID'] = str(inventory.pk) - result, stdout, stderr = self.run_command('acom_inventory', list=True) - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - - def test_without_list_or_host_argument(self): - inventory = self.inventories[0] - self.assertTrue(inventory.active) - os.environ['ACOM_INVENTORY_ID'] = str(inventory.pk) - result, stdout, stderr = self.run_command('acom_inventory') - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - - def test_with_both_list_and_host_arguments(self): - inventory = self.inventories[0] - self.assertTrue(inventory.active) - os.environ['ACOM_INVENTORY_ID'] = str(inventory.pk) - result, stdout, stderr = self.run_command('acom_inventory', list=True, - host='blah') - self.assertTrue(isinstance(result, CommandError)) - self.assertEqual(json.loads(stdout), {}) - -class AcomCallbackEventTest(BaseCommandTest): - ''' - Test cases for acom_callback_event management command. - ''' - - def setUp(self): - super(AcomCallbackEventTest, self).setUp() - self.setup_users() - self.organization = self.make_organizations(self.super_django_user, 1)[0] - self.project = self.make_projects(self.normal_django_user, 1)[0] - self.organization.projects.add(self.project) - self.inventory = Inventory.objects.create(name='test-inventory', - organization=self.organization) - self.host = self.inventory.hosts.create(name='host.example.com', - inventory=self.inventory) - self.group = self.inventory.groups.create(name='test-group', - inventory=self.inventory) - self.group.hosts.add(self.host) - self.job = Job.objects.create(name='job-%s' % now().isoformat(), - inventory=self.inventory, - project=self.project) - self.valid_kwargs = { - 'job_id': self.job.id, - 'event_type': 'playbook_on_start', - 'event_data_json': json.dumps({'test_event_data': [2,4,6]}), - } - - def test_with_job_status_not_running(self): - # Events can only be added when the job is running. - self.assertEqual(self.job.status, 'new') - self.job.status = 'pending' - self.job.save() - result, stdout, stderr = self.run_command('acom_callback_event', - **self.valid_kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('unable to add event ' in str(result).lower()) - self.job.status = 'successful' - self.job.save() - result, stdout, stderr = self.run_command('acom_callback_event', - **self.valid_kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('unable to add event ' in str(result).lower()) - self.job.status = 'failed' - self.job.save() - result, stdout, stderr = self.run_command('acom_callback_event', - **self.valid_kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('unable to add event ' in str(result).lower()) - - def test_with_invalid_args(self): - self.job.status = 'running' - self.job.save() - # Event type not given. - kwargs = dict(self.valid_kwargs.items()) - kwargs.pop('event_type') - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('no event specified' in str(result).lower()) - # Invalid event type. - kwargs = dict(self.valid_kwargs.items()) - kwargs['event_type'] = 'invalid_event_type' - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('unsupported event' in str(result).lower()) - # Neither file or data specified. - kwargs = dict(self.valid_kwargs.items()) - kwargs.pop('event_data_json') - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('either --file or --data' in str(result).lower()) - # Non-integer job ID. - kwargs = dict(self.valid_kwargs.items()) - kwargs['job_id'] = 'foo' - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('id must be an integer' in str(result).lower()) - # No job ID. - kwargs = dict(self.valid_kwargs.items()) - kwargs.pop('job_id') - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('no job id' in str(result).lower()) - # Invalid job ID. - kwargs = dict(self.valid_kwargs.items()) - kwargs['job_id'] = 9999 - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('not found' in str(result).lower()) - # Invalid inline JSON data. - kwargs = dict(self.valid_kwargs.items()) - kwargs['event_data_json'] = 'invalid json' - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('error parsing json' in str(result).lower()) - # Invalid file specified. - kwargs = dict(self.valid_kwargs.items()) - kwargs.pop('event_data_json') - h, tf = tempfile.mkstemp() - os.close(h) - os.remove(tf) - kwargs['event_data_file'] = '%s.json' % tf - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertTrue(isinstance(result, CommandError)) - self.assertTrue('reading from' in str(result).lower()) - - def test_with_valid_args(self): - self.job.status = 'running' - self.job.save() - # Default valid args. - kwargs = dict(self.valid_kwargs.items()) - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertEqual(result, None) - self.assertEqual(self.job.job_events.count(), 1) - # Pass job ID in environment instead. - kwargs = dict(self.valid_kwargs.items()) - kwargs.pop('job_id') - os.environ['ACOM_JOB_ID'] = str(self.job.id) - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertEqual(result, None) - self.assertEqual(self.job.job_events.count(), 2) - os.environ.pop('ACOM_JOB_ID', None) - # Test with JSON data in a file instead. - kwargs = dict(self.valid_kwargs.items()) - kwargs.pop('event_data_json') - h, tf = tempfile.mkstemp(suffix='.json') - self._temp_files.append(tf) - f = os.fdopen(h, 'w') - json.dump({'some_event_data': [1, 2, 3]}, f) - f.close() - kwargs['event_data_file'] = tf - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertEqual(result, None) - self.assertEqual(self.job.job_events.count(), 3) - # Test with JSON data from stdin. - kwargs = dict(self.valid_kwargs.items()) - kwargs.pop('event_data_json') - kwargs['event_data_file'] = '-' - kwargs['stdin_fileobj'] = StringIO.StringIO(json.dumps({'blah': 'bleep'})) - result, stdout, stderr = self.run_command('acom_callback_event', **kwargs) - self.assertEqual(result, None) - self.assertEqual(self.job.job_events.count(), 4) diff --git a/ansibleworks/main/tests/jobs.py b/ansibleworks/main/tests/jobs.py index b76707bd20..87765176fe 100644 --- a/ansibleworks/main/tests/jobs.py +++ b/ansibleworks/main/tests/jobs.py @@ -747,17 +747,15 @@ MIDDLEWARE_CLASSES = filter(lambda x: not x.endswith('TransactionMiddleware'), CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, ANSIBLE_TRANSPORT='local', MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES) -class JobStartCancelTest(BaseJobTestMixin, django.test.TransactionTestCase): +class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): '''Job API tests that need to use the celery task backend.''' def setUp(self): super(JobStartCancelTest, self).setUp() - # Pass test database name in environment for use by the inventory script. - os.environ['ACOM_TEST_DATABASE_NAME'] = settings.DATABASES['default']['NAME'] + settings.INTERNAL_API_URL = self.live_server_url def tearDown(self): super(JobStartCancelTest, self).tearDown() - os.environ.pop('ACOM_TEST_DATABASE_NAME', None) def test_job_start(self): job = self.job_ops_east_run diff --git a/ansibleworks/main/tests/tasks.py b/ansibleworks/main/tests/tasks.py index 9ee688e2e7..33bd99d897 100644 --- a/ansibleworks/main/tests/tasks.py +++ b/ansibleworks/main/tests/tasks.py @@ -7,7 +7,7 @@ import tempfile from django.conf import settings from django.test.utils import override_settings from ansibleworks.main.models import * -from ansibleworks.main.tests.base import BaseTransactionTest, BaseLiveServerTest +from ansibleworks.main.tests.base import BaseLiveServerTest from ansibleworks.main.tasks import RunJob TEST_PLAYBOOK = '''- hosts: test-group @@ -90,7 +90,7 @@ TEST_SSH_KEY_DATA_UNLOCK = 'unlockme' @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True) -class BaseCeleryTest(BaseLiveServerTest):#BaseTransactionTest): +class BaseCeleryTest(BaseLiveServerTest): ''' Base class for celery task tests. ''' @@ -116,8 +116,6 @@ class RunJobTest(BaseCeleryTest): self.group.hosts.add(self.host) self.project = None self.credential = None - # Pass test database name in environment for use by the inventory script. - os.environ['ACOM_TEST_DATABASE_NAME'] = settings.DATABASES['default']['NAME'] # Monkeypatch RunJob to capture list of command line arguments. self.original_build_args = RunJob.build_args self.run_job_args = None @@ -132,7 +130,6 @@ class RunJobTest(BaseCeleryTest): def tearDown(self): super(RunJobTest, self).tearDown() - os.environ.pop('ACOM_TEST_DATABASE_NAME', None) if self.test_project_path: shutil.rmtree(self.test_project_path, True) RunJob.build_args = self.original_build_args diff --git a/ansibleworks/main/views.py b/ansibleworks/main/views.py index 5a849fc71a..c7e74e8c6a 100644 --- a/ansibleworks/main/views.py +++ b/ansibleworks/main/views.py @@ -996,7 +996,8 @@ class InventoryScriptView(generics.RetrieveAPIView): ''' model = Inventory - authentication_classes = [JobCallbackAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES + authentication_classes = [JobCallbackAuthentication] + \ + api_settings.DEFAULT_AUTHENTICATION_CLASSES permission_classes = (JobCallbackPermission,) filter_backends = () @@ -1239,6 +1240,25 @@ class GroupJobEventsList(BaseJobEventsList): class JobJobEventsList(BaseJobEventsList): parent_model = Job + authentication_classes = [JobCallbackAuthentication] + \ + api_settings.DEFAULT_AUTHENTICATION_CLASSES + permission_classes = (JobCallbackPermission,) + + # Post allowed for job event callback only. + def post(self, request, *args, **kwargs): + parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) + data = request.DATA.copy() + data['job'] = parent_obj.pk + serializer = self.get_serializer(data=data) + if serializer.is_valid(): + self.pre_save(serializer.object) + self.object = serializer.save(force_insert=True) + self.post_save(self.object, created=True) + headers = {'Location': serializer.data['url']} + return Response(serializer.data, status=status.HTTP_201_CREATED, + headers=headers) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + # Create view functions for all of the class-based views to simplify inclusion # in URL patterns and reverse URL lookups, converting CamelCase names to diff --git a/ansibleworks/plugins/callback/acom_callback.py b/ansibleworks/plugins/callback/job_event_callback.py similarity index 79% rename from ansibleworks/plugins/callback/acom_callback.py rename to ansibleworks/plugins/callback/job_event_callback.py index 30e880ce8f..e17ad634c0 100644 --- a/ansibleworks/plugins/callback/acom_callback.py +++ b/ansibleworks/plugins/callback/job_event_callback.py @@ -29,14 +29,27 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +# Python import json import os -import subprocess -import sys +import urllib +import urlparse + +# Requests +import requests + +class TokenAuth(requests.auth.AuthBase): + + def __init__(self, token): + self.token = token + + def __call__(self, request): + request.headers['Authorization'] = 'Token %s' % self.token + return request class CallbackModule(object): ''' - Callback module for logging ansible-playbook events to the database. + Callback module for logging ansible-playbook job events via the REST API. ''' # These events should never have an associated play. @@ -55,7 +68,31 @@ class CallbackModule(object): ] def __init__(self): - self.callback_script = os.getenv('ACOM_CALLBACK_EVENT_SCRIPT') + self.job_id = int(os.getenv('JOB_ID')) + self.base_url = os.getenv('REST_API_URL') + self.auth_token = os.getenv('REST_API_TOKEN', '') + + def _post_data(self, event, event_data): + data = json.dumps({ + 'event': event, + 'event_data': event_data, + }) + parts = urlparse.urlsplit(self.base_url) + if parts.username and parts.password: + auth = (parts.username, parts.password) + elif self.auth_token: + auth = TokenAuth(self.auth_token) + else: + auth = None + url = urlparse.urlunsplit([parts.scheme, + '%s:%d' % (parts.hostname, parts.port), + parts.path, parts.query, parts.fragment]) + url_path = '/api/v1/jobs/%d/job_events/' % self.job_id + url = urlparse.urljoin(url, url_path) + headers = {'content-type': 'application/json'} + response = requests.post(url, data=data, headers=headers, auth=auth) + print response.content + response.raise_for_status() def _log_event(self, event, **event_data): play = getattr(getattr(self, 'play', None), 'name', '') @@ -64,9 +101,7 @@ class CallbackModule(object): task = getattr(getattr(self, 'task', None), 'name', '') if task and event not in self.EVENTS_WITHOUT_TASK: event_data['task'] = task - event_data_json = json.dumps(event_data) - cmdline = [self.callback_script, '-e', event, '-d', event_data_json] - subprocess.check_call(cmdline) + self._post_data(event, event_data) def on_any(self, *args, **kwargs): pass diff --git a/app_setup/templates/local_settings.py.j2 b/app_setup/templates/local_settings.py.j2 index 71f28124e1..70eb79929b 100644 --- a/app_setup/templates/local_settings.py.j2 +++ b/app_setup/templates/local_settings.py.j2 @@ -22,7 +22,7 @@ DATABASES = { } } -if 'test' in sys.argv or 'ACOM_TEST_DATABASE_NAME' in os.environ: +if 'test' in sys.argv: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3',