mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Update callback plugin to use API, remove inventory/callback management commands.
This commit is contained in:
parent
59d1ae7322
commit
1d3bd62bd2
@ -1,2 +0,0 @@
|
||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||
# All Rights Reserved.
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 *
|
||||
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user