class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin): class Meta: app_label = 'main' ordering = ('id',) system_job_template = models.ForeignKey( 'SystemJobTemplate', related_name='jobs', blank=True, null=True, default=None, on_delete=models.SET_NULL, ) extra_vars = prevent_search(models.TextField( blank=True, default='', )) extra_vars_dict = VarsDictProperty('extra_vars', True) @classmethod def _get_parent_field_name(cls): return 'system_job_template' @classmethod def _get_task_class(cls): from awx.main.tasks import RunSystemJob return RunSystemJob def websocket_emit_data(self): return {} def get_absolute_url(self, request=None): return reverse('api:system_job_detail', kwargs={'pk': self.pk}, request=request) def get_ui_url(self): return urljoin(settings.TOWER_URL_BASE, "/#/jobs/system/{}".format(self.pk)) @property def event_class(self): return SystemJobEvent @property def task_impact(self): return 5 @property def preferred_instance_groups(self): return self.global_instance_groups ''' JobNotificationMixin ''' def get_notification_templates(self): return self.system_job_template.notification_templates def get_notification_friendly_name(self): return "System Job"
class WorkflowJobOptions(BaseModel): class Meta: abstract = True extra_vars = accepts_json( prevent_search(models.TextField( blank=True, default='', ))) allow_simultaneous = models.BooleanField(default=False) extra_vars_dict = VarsDictProperty('extra_vars', True) @property def workflow_nodes(self): raise NotImplementedError() @classmethod def _get_unified_job_field_names(cls): return set(f.name for f in WorkflowJobOptions._meta.fields) | set( # NOTE: if other prompts are added to WFJT, put fields in WJOptions, remove inventory [ 'name', 'description', 'schedule', 'survey_passwords', 'labels', 'inventory' ]) def _create_workflow_nodes(self, old_node_list, user=None): node_links = {} for old_node in old_node_list: if user: new_node = old_node.create_wfjt_node_copy( user, workflow_job_template=self) else: new_node = old_node.create_workflow_job_node(workflow_job=self) node_links[old_node.pk] = new_node return node_links def _inherit_node_relationships(self, old_node_list, node_links): for old_node in old_node_list: new_node = node_links[old_node.pk] for relationship in [ 'always_nodes', 'success_nodes', 'failure_nodes' ]: old_manager = getattr(old_node, relationship) for old_child_node in old_manager.all(): new_child_node = node_links[old_child_node.pk] new_manager = getattr(new_node, relationship) new_manager.add(new_child_node) def copy_nodes_from_original(self, original=None, user=None): old_node_list = original.workflow_nodes.prefetch_related( 'always_nodes', 'success_nodes', 'failure_nodes').all() node_links = self._create_workflow_nodes(old_node_list, user=user) self._inherit_node_relationships(old_node_list, node_links) def create_relaunch_workflow_job(self): new_workflow_job = self.copy_unified_job() if self.unified_job_template_id is None: new_workflow_job.copy_nodes_from_original(original=self) return new_workflow_job
class WorkflowJobOptions(BaseModel): class Meta: abstract = True extra_vars = prevent_search(models.TextField( blank=True, default='', )) allow_simultaneous = models.BooleanField(default=False) extra_vars_dict = VarsDictProperty('extra_vars', True) @property def workflow_nodes(self): raise NotImplementedError() def _create_workflow_nodes(self, old_node_list, user=None): node_links = {} for old_node in old_node_list: if user: new_node = old_node.create_wfjt_node_copy( user, workflow_job_template=self) else: new_node = old_node.create_workflow_job_node(workflow_job=self) node_links[old_node.pk] = new_node return node_links def _inherit_node_relationships(self, old_node_list, node_links): for old_node in old_node_list: new_node = node_links[old_node.pk] for relationship in [ 'always_nodes', 'success_nodes', 'failure_nodes' ]: old_manager = getattr(old_node, relationship) for old_child_node in old_manager.all(): new_child_node = node_links[old_child_node.pk] new_manager = getattr(new_node, relationship) new_manager.add(new_child_node) def copy_nodes_from_original(self, original=None, user=None): old_node_list = original.workflow_nodes.prefetch_related( 'always_nodes', 'success_nodes', 'failure_nodes').all() node_links = self._create_workflow_nodes(old_node_list, user=user) self._inherit_node_relationships(old_node_list, node_links) def create_relaunch_workflow_job(self): new_workflow_job = self.copy_unified_job() if self.workflow_job_template is None: new_workflow_job.copy_nodes_from_original(original=self) return new_workflow_job
def inventory_source_vars_forward(apps, schema_editor): InventorySource = apps.get_model("main", "InventorySource") ''' The Django app registry does not keep track of model inheritance. The source_vars_dict property comes from InventorySourceOptions via inheritance. This adds that property. Luckily, other properteries and functionality from InventorySourceOptions is not needed by the injector logic. ''' setattr(InventorySource, 'source_vars_dict', VarsDictProperty('source_vars')) source_vars_backup = dict() for inv_source_obj in _get_inventory_sources(InventorySource): if inv_source_obj.source in FrozenInjectors: source_vars_backup[inv_source_obj.id] = dict( inv_source_obj.source_vars_dict) injector = FrozenInjectors[inv_source_obj.source]() new_inv_source_vars = injector.inventory_as_dict( inv_source_obj, None) inv_source_obj.source_vars = yaml.dump(new_inv_source_vars) inv_source_obj.save()
class JobOptions(BaseModel): ''' Common options for job templates and jobs. ''' class Meta: abstract = True diff_mode = models.BooleanField( default=False, help_text= _("If enabled, textual changes made to any templated files on the host are shown in the standard output" ), ) job_type = models.CharField( max_length=64, choices=JOB_TYPE_CHOICES, default='run', ) inventory = models.ForeignKey( 'Inventory', related_name='%(class)ss', blank=True, null=True, default=None, on_delete=models.SET_NULL, ) project = models.ForeignKey( 'Project', related_name='%(class)ss', null=True, default=None, blank=True, on_delete=models.SET_NULL, ) playbook = models.CharField( max_length=1024, default='', blank=True, ) forks = models.PositiveIntegerField( blank=True, default=0, ) limit = models.TextField( blank=True, default='', ) verbosity = models.PositiveIntegerField( choices=VERBOSITY_CHOICES, blank=True, default=0, ) extra_vars = prevent_search(models.TextField( blank=True, default='', )) job_tags = models.CharField( max_length=1024, blank=True, default='', ) force_handlers = models.BooleanField( blank=True, default=False, ) skip_tags = models.CharField( max_length=1024, blank=True, default='', ) start_at_task = models.CharField( max_length=1024, blank=True, default='', ) become_enabled = models.BooleanField(default=False, ) allow_simultaneous = models.BooleanField(default=False, ) timeout = models.IntegerField( blank=True, default=0, help_text= _("The amount of time (in seconds) to run before the task is canceled." ), ) use_fact_cache = models.BooleanField( default=False, help_text= _("If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting " "facts at the end of a playbook run to the database and caching facts for use by Ansible." ), ) extra_vars_dict = VarsDictProperty('extra_vars', True) def clean_credential(self): cred = self.credential if cred and cred.kind != 'ssh': raise ValidationError(_('You must provide an SSH credential.'), ) return cred def clean_vault_credential(self): cred = self.vault_credential if cred and cred.kind != 'vault': raise ValidationError(_('You must provide a Vault credential.'), ) return cred @property def network_credentials(self): return list(self.credentials.filter(credential_type__kind='net')) @property def cloud_credentials(self): return list(self.credentials.filter(credential_type__kind='cloud')) @property def vault_credentials(self): return list(self.credentials.filter(credential_type__kind='vault')) @property def credential(self): cred = self.get_deprecated_credential('ssh') if cred is not None: return cred.pk @property def vault_credential(self): cred = self.get_deprecated_credential('vault') if cred is not None: return cred.pk def get_deprecated_credential(self, kind): for cred in self.credentials.all(): if cred.credential_type.kind == kind: return cred else: return None # TODO: remove when API v1 is removed @property def cloud_credential(self): try: return self.cloud_credentials[-1].pk except IndexError: return None # TODO: remove when API v1 is removed @property def network_credential(self): try: return self.network_credentials[-1].pk except IndexError: return None @property def passwords_needed_to_start(self): '''Return list of password field names needed to start the job.''' needed = [] # Unsaved credential objects can not require passwords if not self.pk: return needed for cred in self.credentials.all(): needed.extend(cred.passwords_needed) return needed
class AdHocCommand(UnifiedJob, JobNotificationMixin): class Meta(object): app_label = 'main' diff_mode = models.BooleanField(default=False, ) job_type = models.CharField( max_length=64, choices=AD_HOC_JOB_TYPE_CHOICES, default='run', ) inventory = models.ForeignKey( 'Inventory', related_name='ad_hoc_commands', null=True, on_delete=models.SET_NULL, ) limit = models.TextField( blank=True, default='', ) credential = models.ForeignKey( 'Credential', related_name='ad_hoc_commands', null=True, default=None, on_delete=models.SET_NULL, ) module_name = models.CharField( max_length=1024, default='', blank=True, ) module_args = models.TextField( blank=True, default='', ) forks = models.PositiveIntegerField( blank=True, default=0, ) verbosity = models.PositiveIntegerField( choices=VERBOSITY_CHOICES, blank=True, default=0, ) become_enabled = models.BooleanField(default=False, ) hosts = models.ManyToManyField( 'Host', related_name='ad_hoc_commands', editable=False, through='AdHocCommandEvent', ) extra_vars = prevent_search(models.TextField( blank=True, default='', )) extra_vars_dict = VarsDictProperty('extra_vars', True) def clean_inventory(self): inv = self.inventory if not inv: raise ValidationError(_('No valid inventory.')) return inv def clean_credential(self): cred = self.credential if cred and cred.kind != 'ssh': raise ValidationError( _('You must provide a machine / SSH credential.'), ) return cred def clean_limit(self): # FIXME: Future feature - check if no hosts would match and reject the # command, instead of having to run it to find out. return self.limit def clean_module_name(self): if type(self.module_name) is not str: raise ValidationError(_("Invalid type for ad hoc command")) module_name = self.module_name.strip() or 'command' if module_name not in settings.AD_HOC_COMMANDS: raise ValidationError(_('Unsupported module for ad hoc commands.')) return module_name def clean_module_args(self): if type(self.module_args) is not str: raise ValidationError(_("Invalid type for ad hoc command")) module_args = self.module_args if self.module_name in ('command', 'shell') and not module_args: raise ValidationError( _('No argument passed to %s module.') % self.module_name) return module_args @property def event_class(self): return AdHocCommandEvent @property def passwords_needed_to_start(self): '''Return list of password field names needed to start the job.''' if self.credential: return self.credential.passwords_needed else: return [] def _get_parent_field_name(self): return '' @classmethod def _get_task_class(cls): from awx.main.tasks import RunAdHocCommand return RunAdHocCommand @classmethod def supports_isolation(cls): return True def get_absolute_url(self, request=None): return reverse('api:ad_hoc_command_detail', kwargs={'pk': self.pk}, request=request) def get_ui_url(self): return urljoin(settings.TOWER_URL_BASE, "/#/jobs/command/{}".format(self.pk)) @property def notification_templates(self): all_orgs = set() for h in self.hosts.all(): all_orgs.add(h.inventory.organization) active_templates = dict(error=set(), success=set(), any=set()) base_notification_templates = NotificationTemplate.objects for org in all_orgs: for templ in base_notification_templates.filter( organization_notification_templates_for_errors=org): active_templates['error'].add(templ) for templ in base_notification_templates.filter( organization_notification_templates_for_success=org): active_templates['success'].add(templ) for templ in base_notification_templates.filter( organization_notification_templates_for_any=org): active_templates['any'].add(templ) active_templates['error'] = list(active_templates['error']) active_templates['any'] = list(active_templates['any']) active_templates['success'] = list(active_templates['success']) return active_templates def get_passwords_needed_to_start(self): return self.passwords_needed_to_start @property def task_impact(self): # NOTE: We sorta have to assume the host count matches and that forks default to 5 from awx.main.models.inventory import Host count_hosts = Host.objects.filter( enabled=True, inventory__ad_hoc_commands__pk=self.pk).count() return min(count_hosts, 5 if self.forks == 0 else self.forks) + 1 def copy(self): data = {} for field in ('job_type', 'inventory_id', 'limit', 'credential_id', 'module_name', 'module_args', 'forks', 'verbosity', 'extra_vars', 'become_enabled', 'diff_mode'): data[field] = getattr(self, field) return AdHocCommand.objects.create(**data) def save(self, *args, **kwargs): update_fields = kwargs.get('update_fields', []) if not self.name: self.name = Truncator(u': '.join( filter(None, (self.module_name, self.module_args)))).chars(512) if 'name' not in update_fields: update_fields.append('name') super(AdHocCommand, self).save(*args, **kwargs) @property def preferred_instance_groups(self): if self.inventory is not None and self.inventory.organization is not None: organization_groups = [ x for x in self.inventory.organization.instance_groups.all() ] else: organization_groups = [] if self.inventory is not None: inventory_groups = [ x for x in self.inventory.instance_groups.all() ] else: inventory_groups = [] selected_groups = inventory_groups + organization_groups if not selected_groups: return self.global_instance_groups return selected_groups ''' JobNotificationMixin ''' def get_notification_templates(self): return self.notification_templates def get_notification_friendly_name(self): return "AdHoc Command"
class JobOptions(BaseModel): ''' Common options for job templates and jobs. ''' class Meta: abstract = True diff_mode = models.BooleanField( default=False, help_text= _("If enabled, textual changes made to any templated files on the host are shown in the standard output" ), ) job_type = models.CharField( max_length=64, choices=JOB_TYPE_CHOICES, default='run', ) inventory = models.ForeignKey( 'Inventory', related_name='%(class)ss', blank=True, null=True, default=None, on_delete=models.SET_NULL, ) project = models.ForeignKey( 'Project', related_name='%(class)ss', null=True, default=None, blank=True, on_delete=models.SET_NULL, ) playbook = models.CharField( max_length=1024, default='', blank=True, ) scm_branch = models.CharField( max_length=1024, default='', blank=True, help_text=_( 'Branch to use in job run. Project default used if blank. ' 'Only allowed if project allow_override field is set to true.'), ) forks = models.PositiveIntegerField( blank=True, default=0, ) limit = models.TextField( blank=True, default='', ) verbosity = models.PositiveIntegerField( choices=VERBOSITY_CHOICES, blank=True, default=0, ) extra_vars = prevent_search( accepts_json(models.TextField( blank=True, default='', ))) job_tags = models.CharField( max_length=1024, blank=True, default='', ) force_handlers = models.BooleanField( blank=True, default=False, ) skip_tags = models.CharField( max_length=1024, blank=True, default='', ) start_at_task = models.CharField( max_length=1024, blank=True, default='', ) become_enabled = models.BooleanField(default=False, ) allow_simultaneous = models.BooleanField(default=False, ) timeout = models.IntegerField( blank=True, default=0, help_text= _("The amount of time (in seconds) to run before the task is canceled." ), ) use_fact_cache = models.BooleanField( default=False, help_text= _("If enabled, Tower will act as an Ansible Fact Cache Plugin; persisting " "facts at the end of a playbook run to the database and caching facts for use by Ansible." ), ) extra_vars_dict = VarsDictProperty('extra_vars', True) @property def machine_credential(self): return self.credentials.filter(credential_type__kind='ssh').first() @property def network_credentials(self): return list(self.credentials.filter(credential_type__kind='net')) @property def cloud_credentials(self): return list(self.credentials.filter(credential_type__kind='cloud')) @property def vault_credentials(self): return list(self.credentials.filter(credential_type__kind='vault')) @property def passwords_needed_to_start(self): '''Return list of password field names needed to start the job.''' needed = [] # Unsaved credential objects can not require passwords if not self.pk: return needed for cred in self.credentials.all(): needed.extend(cred.passwords_needed) return needed