mirror of
https://github.com/ansible/awx.git
synced 2026-05-21 15:57:52 -02:30
Feature: saved launchtime configurations
Consolidate prompts accept/reject logic in unified models Break out accept/reject logic for variables Surface new promptable fields on WFJT nodes, schedules Make schedules and workflows accurately reject variables that are not allowed by the prompting rules or the survey rules on the template Validate against unallowed extra_data in system job schedules Prevent schedule or WFJT node POST/PATCH with unprompted data Move system job days validation to new mechanism Add new psuedo-field for WFJT node credential Add validation for node related credentials Add related config model to unified job Use JobLaunchConfig model for launch RBAC check Support credential overwrite behavior with multi-creds change modern manual launch to use merge behavior Refactor JobLaunchSerializer, self.instance=None Modularize job launch view to create "modern" data Auto-create config object with every job Add create schedule endpoint for jobs
This commit is contained in:
@@ -21,6 +21,9 @@ from django.utils.encoding import smart_text
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# REST Framework
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
# Django-Polymorphic
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
@@ -29,16 +32,16 @@ from django_celery_results.models import TaskResult
|
||||
|
||||
# AWX
|
||||
from awx.main.models.base import * # noqa
|
||||
from awx.main.models.schedules import Schedule
|
||||
from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin
|
||||
from awx.main.utils import (
|
||||
decrypt_field, _inventory_updates,
|
||||
copy_model_by_class, copy_m2m_relationships,
|
||||
get_type_for_model, parse_yaml_or_json
|
||||
get_type_for_model, parse_yaml_or_json,
|
||||
cached_subclassproperty
|
||||
)
|
||||
from awx.main.redact import UriCleaner, REPLACE_STR
|
||||
from awx.main.consumers import emit_channel_notification
|
||||
from awx.main.fields import JSONField
|
||||
from awx.main.fields import JSONField, AskForField
|
||||
|
||||
__all__ = ['UnifiedJobTemplate', 'UnifiedJob']
|
||||
|
||||
@@ -251,6 +254,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
return self.last_job_run
|
||||
|
||||
def update_computed_fields(self):
|
||||
Schedule = self._meta.get_field('schedules').related_model
|
||||
related_schedules = Schedule.objects.filter(enabled=True, unified_job_template=self, next_run__isnull=False).order_by('-next_run')
|
||||
if related_schedules.exists():
|
||||
self.next_schedule = related_schedules[0]
|
||||
@@ -340,11 +344,16 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
'''
|
||||
Create a new unified job based on this unified job template.
|
||||
'''
|
||||
original_passwords = kwargs.pop('survey_passwords', {})
|
||||
eager_fields = kwargs.pop('_eager_fields', None)
|
||||
unified_job_class = self._get_unified_job_class()
|
||||
fields = self._get_unified_job_field_names()
|
||||
unallowed_fields = set(kwargs.keys()) - set(fields)
|
||||
if unallowed_fields:
|
||||
raise Exception('Fields {} are not allowed as overrides.'.format(unallowed_fields))
|
||||
|
||||
unified_job = copy_model_by_class(self, unified_job_class, fields, kwargs)
|
||||
|
||||
eager_fields = kwargs.get('_eager_fields', None)
|
||||
if eager_fields:
|
||||
for fd, val in eager_fields.items():
|
||||
setattr(unified_job, fd, val)
|
||||
@@ -357,18 +366,48 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
if hasattr(self, 'survey_spec') and getattr(self, 'survey_enabled', False):
|
||||
password_list = self.survey_password_variables()
|
||||
hide_password_dict = getattr(unified_job, 'survey_passwords', {})
|
||||
hide_password_dict.update(original_passwords)
|
||||
for password in password_list:
|
||||
hide_password_dict[password] = REPLACE_STR
|
||||
unified_job.survey_passwords = hide_password_dict
|
||||
|
||||
unified_job.save()
|
||||
|
||||
# Labels and extra credentials copied here
|
||||
# Labels and credentials copied here
|
||||
if kwargs.get('credentials'):
|
||||
Credential = UnifiedJob._meta.get_field('credentials').related_model
|
||||
cred_dict = Credential.unique_dict(self.credentials.all())
|
||||
prompted_dict = Credential.unique_dict(kwargs['credentials'])
|
||||
# combine prompted credentials with JT
|
||||
cred_dict.update(prompted_dict)
|
||||
kwargs['credentials'] = [cred for cred in cred_dict.values()]
|
||||
|
||||
from awx.main.signals import disable_activity_stream
|
||||
with disable_activity_stream():
|
||||
copy_m2m_relationships(self, unified_job, fields, kwargs=kwargs)
|
||||
|
||||
if 'extra_vars' in kwargs:
|
||||
unified_job.handle_extra_data(kwargs['extra_vars'])
|
||||
|
||||
if not getattr(self, '_deprecated_credential_launch', False):
|
||||
# Create record of provided prompts for relaunch and rescheduling
|
||||
unified_job.create_config_from_prompts(kwargs)
|
||||
|
||||
return unified_job
|
||||
|
||||
@cached_subclassproperty
|
||||
def ask_mapping(cls):
|
||||
mapping = {}
|
||||
for field in cls._meta.fields:
|
||||
if not isinstance(field, AskForField):
|
||||
continue
|
||||
if field.allows_field == '__default__':
|
||||
allows_field = field.name[len('ask_'):-len('_on_launch')]
|
||||
else:
|
||||
allows_field = field.allows_field
|
||||
mapping[allows_field] = field.name
|
||||
return mapping
|
||||
|
||||
@classmethod
|
||||
def _get_unified_jt_copy_names(cls):
|
||||
return cls._get_unified_job_field_names()
|
||||
@@ -389,6 +428,37 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
copy_m2m_relationships(self, unified_jt, fields)
|
||||
return unified_jt
|
||||
|
||||
def _accept_or_ignore_job_kwargs(self, **kwargs):
|
||||
'''
|
||||
Override in subclass if template accepts _any_ prompted params
|
||||
'''
|
||||
return ({}, kwargs, {"all": ["Fields {} are not allowed on launch.".format(kwargs.keys())]})
|
||||
|
||||
def accept_or_ignore_variables(self, data, errors=None):
|
||||
'''
|
||||
If subclasses accept any `variables` or `extra_vars`, they should
|
||||
define _accept_or_ignore_variables to place those variables in the accepted dict,
|
||||
according to the acceptance rules of the template.
|
||||
'''
|
||||
if errors is None:
|
||||
errors = {}
|
||||
if not isinstance(data, dict):
|
||||
try:
|
||||
data = parse_yaml_or_json(data, silent_failure=False)
|
||||
except ParseError as exc:
|
||||
errors['extra_vars'] = [str(exc)]
|
||||
return ({}, data, errors)
|
||||
if hasattr(self, '_accept_or_ignore_variables'):
|
||||
# SurveyJobTemplateMixin cannot override any methods because of
|
||||
# resolution order, forced by how metaclass processes fields,
|
||||
# thus the need for hasattr check
|
||||
return self._accept_or_ignore_variables(data, errors)
|
||||
elif data:
|
||||
errors['extra_vars'] = [
|
||||
_('Variables {list_of_keys} provided, but this template cannot accept variables.'.format(
|
||||
list_of_keys=', '.join(data.keys())))]
|
||||
return ({}, data, errors)
|
||||
|
||||
|
||||
class UnifiedJobTypeStringMixin(object):
|
||||
@classmethod
|
||||
@@ -750,18 +820,64 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
unified_job_class = self.__class__
|
||||
unified_jt_class = self._get_unified_job_template_class()
|
||||
parent_field_name = unified_job_class._get_parent_field_name()
|
||||
|
||||
fields = unified_jt_class._get_unified_job_field_names() + [parent_field_name]
|
||||
unified_job = copy_model_by_class(self, unified_job_class, fields, {})
|
||||
unified_job.launch_type = 'relaunch'
|
||||
|
||||
create_data = {"launch_type": "relaunch"}
|
||||
if limit:
|
||||
unified_job.limit = limit
|
||||
unified_job.save()
|
||||
create_data["limit"] = limit
|
||||
|
||||
prompts = self.launch_prompts()
|
||||
if self.unified_job_template and prompts:
|
||||
prompts['_eager_fields'] = create_data
|
||||
unified_job = self.unified_job_template.create_unified_job(**prompts)
|
||||
else:
|
||||
unified_job = copy_model_by_class(self, unified_job_class, fields, {})
|
||||
for fd, val in create_data.items():
|
||||
setattr(unified_job, fd, val)
|
||||
unified_job.save()
|
||||
|
||||
# Labels coppied here
|
||||
copy_m2m_relationships(self, unified_job, fields)
|
||||
return unified_job
|
||||
|
||||
def launch_prompts(self):
|
||||
'''
|
||||
Return dictionary of prompts job was launched with
|
||||
returns None if unknown
|
||||
'''
|
||||
JobLaunchConfig = self._meta.get_field('launch_config').related_model
|
||||
try:
|
||||
config = self.launch_config
|
||||
return config.prompts_dict()
|
||||
except JobLaunchConfig.DoesNotExist:
|
||||
return None
|
||||
|
||||
def create_config_from_prompts(self, kwargs):
|
||||
'''
|
||||
Create a launch configuration entry for this job, given prompts
|
||||
returns None if it can not be created
|
||||
'''
|
||||
if self.unified_job_template is None:
|
||||
return None
|
||||
JobLaunchConfig = self._meta.get_field('launch_config').related_model
|
||||
config = JobLaunchConfig(job=self)
|
||||
for field_name, value in kwargs.items():
|
||||
if (field_name not in self.unified_job_template.ask_mapping and field_name != 'survey_passwords'):
|
||||
raise Exception('Unrecognized launch config field {}.'.format(field_name))
|
||||
if field_name == 'credentials':
|
||||
continue
|
||||
key = field_name
|
||||
if key == 'extra_vars':
|
||||
key = 'extra_data'
|
||||
setattr(config, key, value)
|
||||
config.save()
|
||||
|
||||
job_creds = (set(kwargs.get('credentials', [])) -
|
||||
set(self.unified_job_template.credentials.all()))
|
||||
if job_creds:
|
||||
config.credentials.add(*job_creds)
|
||||
return config
|
||||
|
||||
def result_stdout_raw_handle(self, attempt=0):
|
||||
"""Return a file-like object containing the standard out of the
|
||||
job's result.
|
||||
@@ -908,6 +1024,19 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
def can_start(self):
|
||||
return bool(self.status in ('new', 'waiting'))
|
||||
|
||||
@property
|
||||
def can_schedule(self):
|
||||
if getattr(self, 'passwords_needed_to_start', None):
|
||||
return False
|
||||
JobLaunchConfig = self._meta.get_field('launch_config').related_model
|
||||
try:
|
||||
self.launch_config
|
||||
if self.unified_job_template is None:
|
||||
return False
|
||||
return True
|
||||
except JobLaunchConfig.DoesNotExist:
|
||||
return False
|
||||
|
||||
@property
|
||||
def task_impact(self):
|
||||
raise NotImplementedError # Implement in subclass.
|
||||
@@ -1025,8 +1154,6 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
opts = dict([(field, kwargs.get(field, '')) for field in needed])
|
||||
if not all(opts.values()):
|
||||
return False
|
||||
if 'extra_vars' in kwargs:
|
||||
self.handle_extra_data(kwargs['extra_vars'])
|
||||
|
||||
# Sanity check: If we are running unit tests, then run synchronously.
|
||||
if getattr(settings, 'CELERY_UNIT_TEST', False):
|
||||
|
||||
Reference in New Issue
Block a user