Update callback plugin to use API, remove inventory/callback management commands.

This commit is contained in:
Chris Church 2013-06-20 12:44:51 -04:00
parent 59d1ae7322
commit 1d3bd62bd2
13 changed files with 80 additions and 681 deletions

View File

@ -1,2 +0,0 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 *

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',