diff --git a/ansibleworks/main/authentication.py b/ansibleworks/main/authentication.py new file mode 100644 index 0000000000..ce7d4a5202 --- /dev/null +++ b/ansibleworks/main/authentication.py @@ -0,0 +1,29 @@ +# Django REST Framework +from rest_framework import authentication +from rest_framework import exceptions + +# AnsibleWorks +from ansibleworks.main.models import Job + +class JobCallbackAuthentication(authentication.BaseAuthentication): + ''' + Custom authentication used for views accessed by the inventory and callback + scripts when running a job. + ''' + + def authenticate(self, request): + auth = authentication.get_authorization_header(request).split() + if len(auth) != 2 or auth[0].lower() != 'token' or '-' not in auth[1]: + return None + job_id, job_key = auth[1].split('-', 1) + try: + job = Job.objects.get(pk=job_id, status='running') + except Job.DoesNotExist: + return None + token = job.callback_auth_token + if auth[1] != token: + raise exceptions.AuthenticationFailed('Invalid job callback token') + return (None, token) + + def authenticate_header(self, request): + return 'Token' diff --git a/ansibleworks/main/models/__init__.py b/ansibleworks/main/models/__init__.py index 554bc27d86..eae5e6858d 100644 --- a/ansibleworks/main/models/__init__.py +++ b/ansibleworks/main/models/__init__.py @@ -2,6 +2,7 @@ # All Rights Reserved. # Python +import hmac import json import os import shlex @@ -814,6 +815,13 @@ class Job(CommonModel): except TaskMeta.DoesNotExist: pass + @property + def callback_auth_token(self): + '''Return temporary auth token used for task callbacks via API.''' + if self.status == 'running': + h = hmac.new(settings.SECRET_KEY, self.created.isoformat()) + return '%d-%s' % (self.pk, h.hexdigest()) + def get_passwords_needed_to_start(self): '''Return list of password field names needed to start the job.''' needed = [] diff --git a/ansibleworks/main/rbac.py b/ansibleworks/main/rbac.py index d9b54f2246..242c89f20b 100644 --- a/ansibleworks/main/rbac.py +++ b/ansibleworks/main/rbac.py @@ -116,3 +116,15 @@ class CustomRbac(permissions.BasePermission): def has_object_permission(self, request, view, obj): return self.has_permission(request, view, obj) + +class JobCallbackPermission(CustomRbac): + + def has_permission(self, request, view, obj=None): + # If another authentication method was used other than the one for job + # callbacks, return True to fall through to the next permission class. + if request.user or not request.auth: + 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 + diff --git a/ansibleworks/main/tasks.py b/ansibleworks/main/tasks.py index 49cda02df4..412cb49c57 100644 --- a/ansibleworks/main/tasks.py +++ b/ansibleworks/main/tasks.py @@ -86,10 +86,13 @@ class RunJob(Task): # answer: TBD env['ACOM_JOB_ID'] = str(job.pk) env['ACOM_INVENTORY_ID'] = str(job.inventory.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 + env['REST_API_TOKEN'] = job.callback_auth_token or '' env['ANSIBLE_NOCOLOR'] = '1' # Prevent output of escape sequences. return env @@ -108,8 +111,11 @@ class RunJob(Task): # it doesn't make sense to rely on ansible-playbook's default of using # the current user. ssh_username = ssh_username or 'root' - inventory_script = self.get_path_to('management', 'commands', - 'acom_inventory.py') + if False: + inventory_script = self.get_path_to('management', 'commands', + 'acom_inventory.py') + else: + inventory_script = self.get_path_to('..', 'scripts', 'inventory.py') args = ['ansible-playbook', '-i', inventory_script] if job.job_type == 'check': args.append('--check') diff --git a/ansibleworks/main/tests/__init__.py b/ansibleworks/main/tests/__init__.py index 3b93e36f53..6571457980 100644 --- a/ansibleworks/main/tests/__init__.py +++ b/ansibleworks/main/tests/__init__.py @@ -6,6 +6,7 @@ from ansibleworks.main.tests.users import UsersTest from ansibleworks.main.tests.inventory import InventoryTest from ansibleworks.main.tests.projects import ProjectsTest from ansibleworks.main.tests.commands import * +from ansibleworks.main.tests.scripts import * from ansibleworks.main.tests.tasks import RunJobTest from ansibleworks.main.tests.jobs import * diff --git a/ansibleworks/main/tests/base.py b/ansibleworks/main/tests/base.py index 3e494aeadb..1087bd2c35 100644 --- a/ansibleworks/main/tests/base.py +++ b/ansibleworks/main/tests/base.py @@ -240,3 +240,8 @@ class BaseTransactionTest(BaseTestMixin, django.test.TransactionTestCase): Base class for tests requiring transactions (or where the test database needs to be accessed by subprocesses). ''' + +class BaseLiveServerTest(BaseTestMixin, django.test.LiveServerTestCase): + ''' + Base class for tests requiring a live test server. + ''' diff --git a/ansibleworks/main/tests/commands.py b/ansibleworks/main/tests/commands.py index 8cbdb12002..5572fa3d2a 100644 --- a/ansibleworks/main/tests/commands.py +++ b/ansibleworks/main/tests/commands.py @@ -13,7 +13,7 @@ from django.utils.timezone import now from ansibleworks.main.models import * from ansibleworks.main.tests.base import BaseTest -__all__ = ['RunCommandAsScriptTest', 'AcomInventoryTest', +__all__ = ['RunCommandAsScriptTest',# 'AcomInventoryTest', 'AcomCallbackEventTest'] class BaseCommandTest(BaseTest): diff --git a/ansibleworks/main/tests/scripts.py b/ansibleworks/main/tests/scripts.py new file mode 100644 index 0000000000..b54b315291 --- /dev/null +++ b/ansibleworks/main/tests/scripts.py @@ -0,0 +1,271 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + +# Python +import json +import os +import StringIO +import subprocess +import sys +import tempfile + +# Django +from django.conf import settings +from django.utils.timezone import now + +# AnsibleWorks +from ansibleworks.main.models import * +from ansibleworks.main.tests.base import BaseLiveServerTest + +__all__ = ['InventoryScriptTest'] + +class BaseScriptTest(BaseLiveServerTest): + ''' + Base class for tests that run external scripts to access the API. + ''' + + def setUp(self): + super(BaseScriptTest, self).setUp() + self._sys_path = [x for x in sys.path] + self._environ = dict(os.environ.items()) + self._temp_files = [] + + def tearDown(self): + super(BaseScriptTest, 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_script(self, name, *args, **options): + ''' + Run an external script and capture its stdout/stderr and return code. + ''' + #stdin_fileobj = options.pop('stdin_fileobj', None) + pargs = [name] + for k,v in options.items(): + pargs.append('%s%s' % ('-' if len(k) == 1 else '--', k)) + if not v is True: + pargs.append(str(v)) + for arg in args: + pargs.append(str(arg)) + proc = subprocess.Popen(pargs, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate() + return proc.returncode, stdout, stderr + +class InventoryScriptTest(BaseScriptTest): + ''' + Test helper to run management command as standalone script. + ''' + + def setUp(self): + super(InventoryScriptTest, 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 run_inventory_script(self, *args, **options): + os.environ.setdefault('REST_API_URL', self.live_server_url) + os.environ.setdefault('REST_API_TOKEN', + self.super_django_user.auth_token.key) + name = os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', + 'inventory.py') + return self.run_script(name, *args, **options) + + def test_without_inventory_id(self): + rc, stdout, stderr = self.run_inventory_script(list=True) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + rc, stdout, stderr = self.run_inventory_script(host=self.hosts[0].name) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + + def test_list_with_inventory_id_as_argument(self): + inventory = self.inventories[0] + self.assertTrue(inventory.active) + rc, stdout, stderr = self.run_inventory_script(list=True, + inventory=inventory.pk) + self.assertEqual(rc, 0, stderr) + 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['INVENTORY_ID'] = str(invalid_id) + rc, stdout, stderr = self.run_inventory_script(list=True, + inventory=inventory.pk) + self.assertEqual(rc, 0, stderr) + data = json.loads(stdout) + + def test_list_with_inventory_id_in_environment(self): + inventory = self.inventories[1] + self.assertTrue(inventory.active) + os.environ['INVENTORY_ID'] = str(inventory.pk) + rc, stdout, stderr = self.run_inventory_script(list=True) + self.assertEqual(rc, 0, stderr) + 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', {}), group.variables_dict) + 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['INVENTORY_ID'] = str(inventory.pk) + rc, stdout, stderr = self.run_inventory_script(host=host.name) + self.assertEqual(rc, 0, stderr) + 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['INVENTORY_ID'] = str(inventory.pk) + rc, stdout, stderr = self.run_inventory_script(host=host.name) + self.assertEqual(rc, 0, stderr) + data = json.loads(stdout) + self.assertEqual(data, host.variables_dict) + + 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['INVENTORY_ID'] = str(inventory.pk) + rc, stdout, stderr = self.run_inventory_script(host=host.name) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + # Invalid hostname not in database. + rc, stdout, stderr = self.run_inventory_script(host='blah.example.com') + self.assertNotEqual(rc, 0, stderr) + 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['INVENTORY_ID'] = str(invalid_id) + rc, stdout, stderr = self.run_inventory_script(list=True) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + os.environ['INVENTORY_ID'] = 'not_an_int' + rc, stdout, stderr = self.run_inventory_script(list=True) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + os.environ['INVENTORY_ID'] = str(invalid_id) + rc, stdout, stderr = self.run_inventory_script(host=self.hosts[1].name) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + os.environ['INVENTORY_ID'] = 'not_an_int' + rc, stdout, stderr = self.run_inventory_script(host=self.hosts[2].name) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + + def test_with_deleted_inventory(self): + inventory = self.inventories[0] + inventory.mark_inactive() + self.assertFalse(inventory.active) + os.environ['INVENTORY_ID'] = str(inventory.pk) + rc, stdout, stderr = self.run_inventory_script(list=True) + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + + def test_without_list_or_host_argument(self): + inventory = self.inventories[0] + self.assertTrue(inventory.active) + os.environ['INVENTORY_ID'] = str(inventory.pk) + rc, stdout, stderr = self.run_inventory_script() + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + + def _test_with_both_list_and_host_arguments(self): + inventory = self.inventories[0] + self.assertTrue(inventory.active) + os.environ['INVENTORY_ID'] = str(inventory.pk) + rc, stdout, stderr = self.run_inventory_script(list=True, host='blah') + self.assertNotEqual(rc, 0, stderr) + self.assertEqual(json.loads(stdout), {}) + \ No newline at end of file diff --git a/ansibleworks/main/tests/tasks.py b/ansibleworks/main/tests/tasks.py index 547d09e0bc..9ee688e2e7 100644 --- a/ansibleworks/main/tests/tasks.py +++ b/ansibleworks/main/tests/tasks.py @@ -7,7 +7,7 @@ import tempfile from django.conf import settings from django.test.utils import override_settings from ansibleworks.main.models import * -from ansibleworks.main.tests.base import BaseTransactionTest +from ansibleworks.main.tests.base import BaseTransactionTest, 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(BaseTransactionTest): +class BaseCeleryTest(BaseLiveServerTest):#BaseTransactionTest): ''' Base class for celery task tests. ''' @@ -128,6 +128,7 @@ class RunJobTest(BaseCeleryTest): self.build_args_callback() return args RunJob.build_args = new_build_args + settings.INTERNAL_API_URL = self.live_server_url def tearDown(self): super(RunJobTest, self).tearDown() @@ -193,6 +194,7 @@ class RunJobTest(BaseCeleryTest): expect_traceback=False): msg = 'job status is %s, expected %s' % (job.status, expected) msg = '%s\nargs:\n%s' % (msg, job.job_args) + msg = '%s\nenv:\n%s' % (msg, job.job_env) if job.result_traceback: msg = '%s\ngot traceback:\n%s' % (msg, job.result_traceback) if job.result_stdout: diff --git a/ansibleworks/main/urls.py b/ansibleworks/main/urls.py index 88bc17b9e6..a23a8419d1 100644 --- a/ansibleworks/main/urls.py +++ b/ansibleworks/main/urls.py @@ -53,6 +53,7 @@ inventory_urls = patterns('ansibleworks.main.views', url(r'^(?P[0-9]+)/groups/$', 'inventory_groups_list'), url(r'^(?P[0-9]+)/root_groups/$', 'inventory_root_groups_list'), url(r'^(?P[0-9]+)/variable_data/$', 'inventory_variable_detail'), + url(r'^(?P[0-9]+)/script/$', 'inventory_script_view'), ) host_urls = patterns('ansibleworks.main.views', diff --git a/ansibleworks/main/views.py b/ansibleworks/main/views.py index 6477c85038..5a849fc71a 100644 --- a/ansibleworks/main/views.py +++ b/ansibleworks/main/views.py @@ -26,6 +26,7 @@ from rest_framework.views import APIView # AnsibleWorks from ansibleworks.main.access import * +from ansibleworks.main.authentication import JobCallbackAuthentication from ansibleworks.main.base_views import * from ansibleworks.main.models import * from ansibleworks.main.rbac import * @@ -983,6 +984,53 @@ class GroupVariableDetail(BaseVariableDetail): model = Group serializer_class = GroupVariableDataSerializer +class InventoryScriptView(generics.RetrieveAPIView): + ''' + Return inventory group and host data as needed for an inventory script. + + Without query parameters, return groups with hosts, children and vars + (equivalent to the --list parameter to an inventory script). + + With ?host=HOSTNAME, return host vars for the given host (equivalent to the + --host HOSTNAME parameter to an inventory script). + ''' + + model = Inventory + authentication_classes = [JobCallbackAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES + permission_classes = (JobCallbackPermission,) + filter_backends = () + + def retrieve(self, request, *args, **kwargs): + self.object = self.get_object() + hostname = request.QUERY_PARAMS.get('host', '') + if hostname: + try: + host = self.object.hosts.get(active=True, name=hostname) + data = host.variables_dict + except Host.DoesNotExist: + raise Http404 + else: + data = {} + for group in self.object.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']): + data[group.name] = group_info.get('hosts', []) + else: + data[group.name] = group_info + if self.object.variables_dict: + data['all'] = { + 'vars': self.object.variables_dict, + } + return Response(data) + class JobTemplateList(BaseList): model = JobTemplate diff --git a/ansibleworks/scripts/inventory.py b/ansibleworks/scripts/inventory.py new file mode 100755 index 0000000000..5234fd24a2 --- /dev/null +++ b/ansibleworks/scripts/inventory.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python + +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + +# Python +import json +import optparse +import os +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 InventoryScript(object): + + def __init__(self, **options): + self.options = options + + def get_data(self): + 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/inventories/%d/script/' % self.inventory_id + if self.hostname: + url_path += '?%s' % urllib.urlencode({'host': self.hostname}) + url = urlparse.urljoin(url, url_path) + response = requests.get(url, auth=auth) + response.raise_for_status() + sys.stdout.write(json.dumps(response.json(), indent=self.indent) + '\n') + + def run(self): + try: + self.base_url = self.options.get('base_url', '') or \ + os.getenv('REST_API_URL', '') + if not self.base_url: + raise ValueError('No REST API URL specified') + self.auth_token = self.options.get('authtoken', '') or \ + os.getenv('REST_API_TOKEN', '') + parts = urlparse.urlsplit(self.base_url) + if not (parts.username and parts.password) and not self.auth_token: + raise ValueError('No REST API token or username/password ' + 'specified') + try: + # Command line argument takes precedence over environment + # variable. + self.inventory_id = int(self.options.get('inventory_id', 0) or \ + os.getenv('INVENTORY_ID', 0)) + except ValueError: + raise ValueError('Inventory ID must be an integer') + if not self.inventory_id: + raise ValueError('No inventory ID specified') + self.hostname = self.options.get('hostname', '') + self.list_ = self.options.get('list', False) + self.indent = self.options.get('indent', None) + if self.list_ and self.hostname: + raise RuntimeError('Only --list or --host may be specified') + elif self.list_ or self.hostname: + self.get_data() + else: + raise RuntimeError('Either --list or --host must be specified') + except Exception, e: + # Always return an empty hash on stdout, even when an error occurs. + sys.stdout.write(json.dumps({})) + #print >> file(os.path.join(os.path.dirname(__file__), 'foo.log'), 'a'), repr(e) + #if hasattr(e, 'response'): + # print >> file(os.path.join(os.path.dirname(__file__), 'foo.log'), 'a'), e.response.content + if self.options.get('traceback', False): + raise + sys.stderr.write(str(e) + '\n') + if hasattr(e, 'response'): + sys.stderr.write(e.response.content + '\n') + sys.exit(1) + +def main(): + parser = optparse.OptionParser() + parser.add_option('-v', '--verbosity', action='store', dest='verbosity', + default='1', type='choice', choices=['0', '1', '2', '3'], + help='Verbosity level; 0=minimal output, 1=normal output' + ', 2=verbose output, 3=very verbose output') + parser.add_option('--traceback', action='store_true', + help='Raise on exception on error') + parser.add_option('-u', '--url', dest='base_url', default='', + help='Base URL to access REST API (can also be specified' + ' using REST_API_URL environment variable)') + parser.add_option('--authtoken', dest='authtoken', default='', + help='Authentication token used to access REST API (can ' + 'also be specified using REST_API_TOKEN environment ' + 'variable)') + parser.add_option('-i', '--inventory', dest='inventory_id', type='int', + default=0, help='Inventory ID (can also be specified ' + 'using INVENTORY_ID environment variable)') + parser.add_option('--list', action='store_true', dest='list', + default=False, help='Return JSON hash of host groups.') + parser.add_option('--host', dest='hostname', default='', + help='Return JSON hash of host vars.') + parser.add_option('--indent', dest='indent', type='int', default=None, + help='Indentation level for pretty printing output') + options, args = parser.parse_args() + InventoryScript(**vars(options)).run() + +if __name__ == '__main__': + main() diff --git a/ansibleworks/settings/defaults.py b/ansibleworks/settings/defaults.py index e608fec71b..7a8003435f 100644 --- a/ansibleworks/settings/defaults.py +++ b/ansibleworks/settings/defaults.py @@ -133,7 +133,6 @@ INSTALLED_APPS = ( INTERNAL_IPS = ('127.0.0.1',) REST_FRAMEWORK = { - 'FILTER_BACKEND': 'ansibleworks.main.custom_filters.CustomFilterBackend', 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'ansibleworks.main.pagination.PaginationSerializer', 'PAGINATE_BY': 25, 'PAGINATE_BY_PARAM': 'page_size', @@ -142,6 +141,9 @@ REST_FRAMEWORK = { 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', ), + 'DEFAULT_FILTER_BACKENDS': ( + 'ansibleworks.main.custom_filters.CustomFilterBackend', + ), 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', @@ -218,6 +220,9 @@ DEVSERVER_MODULES = ( #'devserver.modules.profile.LineProfilerModule', ) +# Set default ports for live server tests. +os.environ.setdefault('DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:9013-9199') + # Skip migrations when running tests. SOUTH_TESTS_MIGRATE = False @@ -234,6 +239,11 @@ CELERYD_TASK_SOFT_TIME_LIMIT = 3540 CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler' CELERYBEAT_MAX_LOOP_INTERVAL = 60 +if 'devserver' in INSTALLED_APPS: + INTERNAL_API_URL = 'http://127.0.0.1:%s' % DEVSERVER_DEFAULT_PORT +else: + INTERNAL_API_URL = 'http://127.0.0.1:8000' + LOGGING = { 'version': 1, 'disable_existing_loggers': False, diff --git a/ansibleworks/settings/production.py b/ansibleworks/settings/production.py index 092c296838..0abfdf7a0c 100644 --- a/ansibleworks/settings/production.py +++ b/ansibleworks/settings/production.py @@ -21,6 +21,8 @@ ALLOWED_HOSTS = [] # Production should only use minified JS for UI. USE_MINIFIED_JS = True +INTERNAL_API_URL = 'http://127.0.0.1:80' + # If a local_settings.py file is present here, use it and ignore the global # settings. Normally, local settings would only be present during development. try: diff --git a/requirements/dev.txt b/requirements/dev.txt index 6c9fd24c3f..b76b7e8863 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -11,6 +11,7 @@ djangorestframework>=2.3.0,<2.4.0 Markdown pexpect python-dateutil +requests South>=0.8,<2.0 django-debug-toolbar diff --git a/requirements/requests-1.2.3.tar.gz b/requirements/requests-1.2.3.tar.gz new file mode 100644 index 0000000000..cf59fbae87 Binary files /dev/null and b/requirements/requests-1.2.3.tar.gz differ diff --git a/setup.py b/setup.py index 097647c2ac..cd8b28b760 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ def proc_data_files(data_files): setup( name='ansibleworks', - version=__version__.split("-")[0], + version=__version__.split("-")[0], # FIXME: Should keep full version here? author='AnsibleWorks, Inc.', author_email='support@ansibleworks.com', description='AnsibleWorks API, UI and Task Engine', @@ -75,6 +75,7 @@ setup( 'pexpect', 'python-dateutil', 'PyYAML', + 'requests', 'South>=0.8,<2.0', ], setup_requires=[],