From 6813a082f0185b7db8dee25820a1c5c2d5c5f034 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 3 Jul 2014 15:59:45 -0400 Subject: [PATCH] Add refresh_inventory flag for job_template callback to refresh inventory before trying to find a matching host. --- .../templates/api/job_template_callback.md | 13 +++++- awx/api/views.py | 42 +++++++++++++++---- awx/main/models/jobs.py | 12 ++++++ awx/ui/static/js/helpers/JobTemplates.js | 2 +- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/awx/api/templates/api/job_template_callback.md b/awx/api/templates/api/job_template_callback.md index bbd26ffdde..ae77c48df1 100644 --- a/awx/api/templates/api/job_template_callback.md +++ b/awx/api/templates/api/job_template_callback.md @@ -1,4 +1,4 @@ -The job template callback allows for empheral hosts to launch a new job. +The job template callback allows for ephemeral hosts to launch a new job. Configure a host to POST to this resource, passing the `host_config_key` parameter, to start a new job limited to only the requesting host. In the @@ -18,6 +18,17 @@ The response will return status 202 if the request is valid, 403 for an invalid host config key, or 400 if the host cannot be determined from the address making the request. +_(New in Ansible Tower 2.0.0)_ By default, the host must already be present in +inventory for the callback to succeed. The `refresh_inventory` parameter can +be passed to the callback to trigger an inventory sync prior to searching for +the host and running the job. The associated inventory must have the +`update_on_launch` flag set and will only refresh if the `update_cache_timeout` +has expired. + +For example, using curl: + + curl --data-urlencode "host_config_key=HOST_CONFIG_KEY&refresh_inventory=1" http://server/api/v1/job_templates/N/callback/ + A GET request may be used to verify that the correct host will be selected. This request must authenticate as a valid user with permission to edit the job template. For example: diff --git a/awx/api/views.py b/awx/api/views.py index af07321c5e..84e00855fb 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -14,8 +14,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.db.models import Q, Count, Sum - -from django.db import IntegrityError +from django.db import IntegrityError, transaction from django.shortcuts import get_object_or_404 from django.utils.datastructures import SortedDict from django.utils.timezone import now @@ -1268,15 +1267,18 @@ class JobTemplateCallback(GenericAPIView): # Find the list of remote host names/IPs to check. remote_hosts = set() for header in settings.REMOTE_HOST_HEADERS: - value = self.request.META.get(header, '').strip() - if value: - remote_hosts.add(value) + for value in self.request.META.get(header, '').split(','): + value = value.strip() + if value: + remote_hosts.add(value) # Add the reverse lookup of IP addresses. for rh in list(remote_hosts): try: result = socket.gethostbyaddr(rh) except socket.herror: continue + except socket.gaierror: + continue remote_hosts.add(result[0]) remote_hosts.update(result[1]) # Filter out any .arpa results. @@ -1336,9 +1338,35 @@ class JobTemplateCallback(GenericAPIView): return Response(data) def post(self, request, *args, **kwargs): - job_template = self.get_object() # Permission class should have already validated host_config_key. + job_template = self.get_object() + # Attempt to find matching hosts based on remote address. matching_hosts = self.find_matching_hosts() + # If refresh_inventory flag is provided and the host is not found, + # update the inventory before trying to match the host. + refresh_inventory = request.DATA.get('refresh_inventory', '') + refresh_inventory = bool(refresh_inventory and refresh_inventory[0].lower() in ('t', 'y', '1')) + inventory_sources_already_updated = [] + if refresh_inventory and len(matching_hosts) != 1: + inventory_sources = job_template.inventory.inventory_sources.filter(active=True, update_on_launch=True) + inventory_update_pks = set() + for inventory_source in inventory_sources: + if inventory_source.needs_update_on_launch: + # FIXME: Doesn't check for any existing updates. + inventory_update = inventory_source.create_inventory_update(launch_type='callback') + inventory_update.signal_start() + inventory_update_pks.add(inventory_update.pk) + inventory_update_qs = InventoryUpdate.objects.filter(pk__in=inventory_update_pks, status__in=('pending', 'waiting', 'running')) + # Poll for the inventory updates we've started to complete. + while inventory_update_qs.count(): + time.sleep(1.0) + transaction.commit() + # Ignore failed inventory updates here, only add successful ones + # to the list to be excluded when running the job. + for inventory_update in InventoryUpdate.objects.filter(pk__in=inventory_update_pks, status='successful'): + inventory_sources_already_updated.append(inventory_update.inventory_source_id) + matching_hosts = self.find_matching_hosts() + # Check matching hosts. if not matching_hosts: data = dict(msg='No matching host could be found!') # FIXME: Log! @@ -1355,7 +1383,7 @@ class JobTemplateCallback(GenericAPIView): return Response(data, status=status.HTTP_400_BAD_REQUEST) limit = ':&'.join(filter(None, [job_template.limit, host.name])) job = job_template.create_job(limit=limit, launch_type='callback') - result = job.signal_start() + result = job.signal_start(inventory_sources_already_updated=inventory_sources_already_updated) if not result: data = dict(msg='Error starting job!') return Response(data, status=status.HTTP_400_BAD_REQUEST) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index a758497892..82ad91090a 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -323,6 +323,18 @@ class Job(UnifiedJob, JobOptions): if type(obj) == InventoryUpdate: if obj.inventory_source in inventory_sources: inventory_sources_found.append(obj.inventory_source) + # Skip updating any inventory sources that were already updated before + # running this job (via callback inventory refresh). + try: + start_args = json.loads(decrypt_field(self, 'start_args')) + except Exception, e: + start_args = None + start_args = start_args or {} + inventory_sources_already_updated = start_args.get('inventory_sources_already_updated', []) + if inventory_sources_already_updated: + for source in inventory_sources.filter(pk__in=inventory_sources_already_updated): + if source not in inventory_sources_found: + inventory_sources_found.append(source) if not project_found and self.project.needs_update_on_launch: dependencies.append(self.project.create_project_update(launch_type='dependency')) if inventory_sources.count(): # and not has_setup_failures? Probably handled as an error scenario in the task runner diff --git a/awx/ui/static/js/helpers/JobTemplates.js b/awx/ui/static/js/helpers/JobTemplates.js index e302a081ff..455e2fb0f3 100644 --- a/awx/ui/static/js/helpers/JobTemplates.js +++ b/awx/ui/static/js/helpers/JobTemplates.js @@ -25,7 +25,7 @@ angular.module('JobTemplatesHelper', ['Utilities']) scope.setCallbackHelp = function() { scope.callback_help = "

With a provisioning callback URL and a host config key a host can contact Tower and request a configuration update using this job " + "template. The request from the host must be a POST. Here is an example using curl:

\n" + - "
curl --data \"host_config_key=\"" + scope.example_config_key + "\" " +
+                "
curl --data \"host_config_key=" + scope.example_config_key + "\" " +
                 scope.callback_server_path + GetBasePath('job_templates') + scope.example_template_id + "/callback/
\n" + "

Note the requesting host must be defined in the inventory associated with the job template. If Tower fails to " + "locate the host, the request will be denied.

" +