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.exceptions import PermissionDenied
from rest_framework import permissions from rest_framework import permissions
from ansibleworks.main.access import * from ansibleworks.main.access import *
from ansibleworks.main.models import *
logger = logging.getLogger('ansibleworks.main.rbac') logger = logging.getLogger('ansibleworks.main.rbac')
@@ -126,5 +127,13 @@ class JobCallbackPermission(CustomRbac):
return super(JobCallbackPermission, self).has_permission(request, view, obj) return super(JobCallbackPermission, self).has_permission(request, view, obj)
# FIXME: Verify that inventory or job event requested are for the same # FIXME: Verify that inventory or job event requested are for the same
# job ID present in the auth token, etc. # 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. Build environment dictionary for ansible-playbook.
''' '''
plugin_dir = self.get_path_to('..', 'plugins', 'callback') 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()) env = dict(os.environ.items())
# question: when running over CLI, generate a random ID or grab next, etc? # question: when running over CLI, generate a random ID or grab next, etc?
# answer: TBD # answer: TBD
env['ACOM_JOB_ID'] = str(job.pk) env['JOB_ID'] = str(job.pk)
env['ACOM_INVENTORY_ID'] = str(job.inventory.pk)
env['INVENTORY_ID'] = str(job.inventory.pk) env['INVENTORY_ID'] = str(job.inventory.pk)
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
env['ACOM_CALLBACK_EVENT_SCRIPT'] = callback_script
if hasattr(settings, 'ANSIBLE_TRANSPORT'): if hasattr(settings, 'ANSIBLE_TRANSPORT'):
env['ANSIBLE_TRANSPORT'] = getattr(settings, 'ANSIBLE_TRANSPORT') env['ANSIBLE_TRANSPORT'] = getattr(settings, 'ANSIBLE_TRANSPORT')
env['REST_API_URL'] = settings.INTERNAL_API_URL 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.users import UsersTest
from ansibleworks.main.tests.inventory import InventoryTest from ansibleworks.main.tests.inventory import InventoryTest
from ansibleworks.main.tests.projects import ProjectsTest from ansibleworks.main.tests.projects import ProjectsTest
from ansibleworks.main.tests.commands import *
from ansibleworks.main.tests.scripts import * from ansibleworks.main.tests.scripts import *
from ansibleworks.main.tests.tasks import RunJobTest from ansibleworks.main.tests.tasks import RunJobTest
from ansibleworks.main.tests.jobs import * 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, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
ANSIBLE_TRANSPORT='local', ANSIBLE_TRANSPORT='local',
MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES) 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.''' '''Job API tests that need to use the celery task backend.'''
def setUp(self): def setUp(self):
super(JobStartCancelTest, self).setUp() super(JobStartCancelTest, self).setUp()
# Pass test database name in environment for use by the inventory script. settings.INTERNAL_API_URL = self.live_server_url
os.environ['ACOM_TEST_DATABASE_NAME'] = settings.DATABASES['default']['NAME']
def tearDown(self): def tearDown(self):
super(JobStartCancelTest, self).tearDown() super(JobStartCancelTest, self).tearDown()
os.environ.pop('ACOM_TEST_DATABASE_NAME', None)
def test_job_start(self): def test_job_start(self):
job = self.job_ops_east_run job = self.job_ops_east_run

View File

@@ -7,7 +7,7 @@ import tempfile
from django.conf import settings from django.conf import settings
from django.test.utils import override_settings from django.test.utils import override_settings
from ansibleworks.main.models import * 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 from ansibleworks.main.tasks import RunJob
TEST_PLAYBOOK = '''- hosts: test-group TEST_PLAYBOOK = '''- hosts: test-group
@@ -90,7 +90,7 @@ TEST_SSH_KEY_DATA_UNLOCK = 'unlockme'
@override_settings(CELERY_ALWAYS_EAGER=True, @override_settings(CELERY_ALWAYS_EAGER=True,
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True) CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class BaseCeleryTest(BaseLiveServerTest):#BaseTransactionTest): class BaseCeleryTest(BaseLiveServerTest):
''' '''
Base class for celery task tests. Base class for celery task tests.
''' '''
@@ -116,8 +116,6 @@ class RunJobTest(BaseCeleryTest):
self.group.hosts.add(self.host) self.group.hosts.add(self.host)
self.project = None self.project = None
self.credential = 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. # Monkeypatch RunJob to capture list of command line arguments.
self.original_build_args = RunJob.build_args self.original_build_args = RunJob.build_args
self.run_job_args = None self.run_job_args = None
@@ -132,7 +130,6 @@ class RunJobTest(BaseCeleryTest):
def tearDown(self): def tearDown(self):
super(RunJobTest, self).tearDown() super(RunJobTest, self).tearDown()
os.environ.pop('ACOM_TEST_DATABASE_NAME', None)
if self.test_project_path: if self.test_project_path:
shutil.rmtree(self.test_project_path, True) shutil.rmtree(self.test_project_path, True)
RunJob.build_args = self.original_build_args RunJob.build_args = self.original_build_args

View File

@@ -996,7 +996,8 @@ class InventoryScriptView(generics.RetrieveAPIView):
''' '''
model = Inventory model = Inventory
authentication_classes = [JobCallbackAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES authentication_classes = [JobCallbackAuthentication] + \
api_settings.DEFAULT_AUTHENTICATION_CLASSES
permission_classes = (JobCallbackPermission,) permission_classes = (JobCallbackPermission,)
filter_backends = () filter_backends = ()
@@ -1239,6 +1240,25 @@ class GroupJobEventsList(BaseJobEventsList):
class JobJobEventsList(BaseJobEventsList): class JobJobEventsList(BaseJobEventsList):
parent_model = Job 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 # Create view functions for all of the class-based views to simplify inclusion
# in URL patterns and reverse URL lookups, converting CamelCase names to # 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 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
# Python
import json import json
import os import os
import subprocess import urllib
import sys 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): 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. # These events should never have an associated play.
@@ -55,7 +68,31 @@ class CallbackModule(object):
] ]
def __init__(self): 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): def _log_event(self, event, **event_data):
play = getattr(getattr(self, 'play', None), 'name', '') play = getattr(getattr(self, 'play', None), 'name', '')
@@ -64,9 +101,7 @@ class CallbackModule(object):
task = getattr(getattr(self, 'task', None), 'name', '') task = getattr(getattr(self, 'task', None), 'name', '')
if task and event not in self.EVENTS_WITHOUT_TASK: if task and event not in self.EVENTS_WITHOUT_TASK:
event_data['task'] = task event_data['task'] = task
event_data_json = json.dumps(event_data) self._post_data(event, event_data)
cmdline = [self.callback_script, '-e', event, '-d', event_data_json]
subprocess.check_call(cmdline)
def on_any(self, *args, **kwargs): def on_any(self, *args, **kwargs):
pass 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 = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',