Updated callback module to delegate to acom_callback_event management command.

This commit is contained in:
Chris Church 2013-04-04 13:59:32 -04:00
parent 0a306ee0ad
commit 1b93886be2
8 changed files with 216 additions and 60 deletions

View File

@ -132,7 +132,7 @@ class CredentialAdmin(admin.ModelAdmin):
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'active')
filter_horizontal = ('projects', 'users', 'organizations', 'tags')
filter_horizontal = ('projects', 'users', 'tags')
class ProjectAdmin(admin.ModelAdmin):

56
lib/main/management/commands/acom_callback_event.py Normal file → Executable file
View File

@ -22,23 +22,40 @@ import json
from optparse import make_option
import os
import sys
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import NoArgsCommand, CommandError
class Command(BaseCommmand):
class Command(NoArgsCommand):
'''
Management command to log callback events from ansible-playbook.
'''
help = 'Ansible Commander Callback Event Capture'
option_list = BaseCommmand.option_list + (
option_list = NoArgsCommand.option_list + (
make_option('-i', '--launch-job-status', dest='launch_job_status_id',
type='int', default=0,
help='Inventory ID (can also be specified using '
'ACOM_INVENTORY_ID environment variable)'),
#make_option('--indent', dest='indent', type='int', default=None,
# help='Indentation level for pretty printing output'),
help='Launch job status ID (can also be specified using '
'ACOM_LAUNCH_JOB_STATUS_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'),
)
def handle(self, *args, **options):
from lib.main.models import LaunchJobStatus
def handle_noargs(self, **options):
from lib.main.models import LaunchJobStatus, LaunchJobStatusEvent
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 LaunchJobStatusEvent.EVENT_TYPES]:
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:
launch_job_status_id = int(os.getenv('ACOM_LAUNCH_JOB_STATUS_ID',
options.get('launch_job_status_id', 0)))
@ -48,9 +65,26 @@ class Command(BaseCommmand):
raise CommandError('No launch job status ID specified')
try:
launch_job_status = LaunchJobStatus.objects.get(id=launch_job_status_id)
except Inventory.DoesNotExist:
except LaunchJobStatus.DoesNotExist:
raise CommandError('Launch job status with ID %d not found' % launch_job_status_id)
# FIXME: Do stuff here.
if launch_job_status.status != 'running':
raise CommandError('Unable to add event except when launch job is running')
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')
launch_job_status.launch_job_status_events.create(event=event_type,
event_data=event_data)
if __name__ == '__main__':
from __init__ import run_command_as_script

View File

@ -21,7 +21,6 @@
import json
from optparse import make_option
import os
import sys
from django.core.management.base import NoArgsCommand, CommandError
class Command(NoArgsCommand):

View File

@ -34,10 +34,14 @@ def run_launch_job(launch_job_status_pk):
inventory_script = os.path.abspath(os.path.join(os.path.dirname(__file__),
'management', 'commands',
'acom_inventory.py'))
callback_script = os.path.abspath(os.path.join(os.path.dirname(__file__),
'management', 'commands',
'acom_callback_event.py'))
env = dict(os.environ.items())
env['ACOM_LAUNCH_JOB_STATUS_ID'] = str(launch_job_status.pk)
env['ACOM_INVENTORY_ID'] = str(launch_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')

View File

@ -20,5 +20,5 @@ from lib.main.tests.organizations import OrganizationsTest
from lib.main.tests.users import UsersTest
from lib.main.tests.inventory import InventoryTest
from lib.main.tests.projects import ProjectsTest
from lib.main.tests.commands import AcomInventoryTest
from lib.main.tests.commands import *
from lib.main.tests.tasks import RunLaunchJobTest

View File

@ -20,11 +20,15 @@ import json
import os
import StringIO
import sys
import tempfile
from django.core.management import call_command
from django.core.management.base import CommandError
from django.utils.timezone import now
from lib.main.models import *
from lib.main.tests.base import BaseTest
__all__ = ['AcomInventoryTest', 'AcomCallbackEventTest']
class BaseCommandTest(BaseTest):
'''
Base class for tests that run management commands.
@ -33,6 +37,7 @@ class BaseCommandTest(BaseTest):
def setUp(self):
super(BaseCommandTest, self).setUp()
self._environ = dict(os.environ.items())
self._temp_files = []
def tearDown(self):
super(BaseCommandTest, self).tearDown()
@ -42,16 +47,23 @@ class BaseCommandTest(BaseTest):
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.
'''
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
@ -64,6 +76,7 @@ class BaseCommandTest(BaseTest):
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
@ -244,4 +257,145 @@ class AcomInventoryTest(BaseCommandTest):
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.launch_job = LaunchJob.objects.create(name='test-launch-job',
inventory=self.inventory,
project=self.project)
self.launch_job_status = self.launch_job.launch_job_statuses.create(
name='launch-job-status-%s' % now().isoformat())
self.valid_kwargs = {
'launch_job_status_id': self.launch_job_status.id,
'event_type': 'playbook_on_start',
'event_data_json': json.dumps({'test_event_data': [2,4,6]}),
}
def test_with_launch_job_status_not_running(self):
# Events can only be added when the launch job is running.
self.assertEqual(self.launch_job_status.status, 'pending')
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.launch_job_status.status = 'successful'
self.launch_job_status.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.launch_job_status.status = 'failed'
self.launch_job_status.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.launch_job_status.status = 'running'
self.launch_job_status.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 launch job status ID.
kwargs = dict(self.valid_kwargs.items())
kwargs['launch_job_status_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 launch job status ID.
kwargs = dict(self.valid_kwargs.items())
kwargs.pop('launch_job_status_id')
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertTrue(isinstance(result, CommandError))
self.assertTrue('no launch job status id' in str(result).lower())
# Invalid launch job status ID.
kwargs = dict(self.valid_kwargs.items())
kwargs['launch_job_status_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.launch_job_status.status = 'running'
self.launch_job_status.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.launch_job_status.launch_job_status_events.count(), 1)
# Pass launch job status in environment instead.
kwargs = dict(self.valid_kwargs.items())
kwargs.pop('launch_job_status_id')
os.environ['ACOM_LAUNCH_JOB_STATUS_ID'] = str(self.launch_job_status.id)
result, stdout, stderr = self.run_command('acom_callback_event', **kwargs)
self.assertEqual(result, None)
self.assertEqual(self.launch_job_status.launch_job_status_events.count(), 2)
os.environ.pop('ACOM_LAUNCH_JOB_STATUS_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.launch_job_status.launch_job_status_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.launch_job_status.launch_job_status_events.count(), 4)

View File

@ -88,12 +88,10 @@ class RunLaunchJobTest(BaseCeleryTest):
launch_job_status = self.launch_job.start()
self.assertEqual(launch_job_status.status, 'pending')
launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status.pk)
print 'stdout:', launch_job_status.result_stdout
print 'stderr:', launch_job_status.result_stderr
print launch_job_status.status
print settings.DATABASES
#print 'stdout:', launch_job_status.result_stdout
#print 'stderr:', launch_job_status.result_stderr
#print launch_job_status.status
#print settings.DATABASES
self.assertEqual(launch_job_status.status, 'successful')
self.assertTrue(launch_job_status.result_stdout)
launch_job_status_events = launch_job_status.launch_job_status_events.all()

View File

@ -16,7 +16,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import subprocess
import sys
class CallbackModule(object):
@ -25,47 +27,12 @@ class CallbackModule(object):
'''
def __init__(self):
# the DJANGO_SETTINGS_MODULE environment variable *should* already
# be set if this callback is called when executing a playbook via a
# celery task, otherwise just bail out.
settings_module_name = os.environ.get('DJANGO_SETTINGS_MODULE', None)
if not settings_module_name:
return
# FIXME: Not particularly fond of this sys.path hack, but it is needed
# when a celery task calls ansible-playbook and needs to execute this
# script directly.
try:
settings_parent_module = __import__(settings_module_name)
except ImportError:
top_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..')
sys.path.insert(0, os.path.abspath(top_dir))
settings_parent_module = __import__(settings_module_name)
settings_module = getattr(settings_parent_module, 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']
# Try to get the launch job status ID from the environment, otherwise
# just bail out now.
try:
launch_job_status_pk = int(os.environ.get('ACOM_LAUNCH_JOB_STATUS_ID', ''))
except ValueError:
return
from lib.main.models import LaunchJobStatus
try:
self.launch_job_status = LaunchJobStatus.objects.get(pk=launch_job_status_pk)
except LaunchJobStatus.DoesNotExist:
pass
self.acom_callback_event_script = os.getenv('ACOM_CALLBACK_EVENT_SCRIPT')
def _log_event(self, event, **event_data):
#print '====', event, args, kwargs
# self.playbook.inventory
if hasattr(self, 'launch_job_status'):
kwargs = {
'event': event,
'event_data': event_data,
}
self.launch_job_status.launch_job_status_events.create(**kwargs)
event_data_json = json.dumps(event_data)
cmdline = [self.acom_callback_event_script, '-e', event, '-d', event_data_json]
subprocess.check_call(cmdline)
def on_any(self, *args, **kwargs):
pass