mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 08:48:46 -03:30
Jobs updates to start/cancel and prompt for passwords via the admin.
This commit is contained in:
@@ -327,11 +327,14 @@ class JobAdmin(BaseModelAdmin):
|
|||||||
(_('More Options'), {'fields': ('use_sudo', 'forks', 'limit',
|
(_('More Options'), {'fields': ('use_sudo', 'forks', 'limit',
|
||||||
'verbosity', 'extra_vars'),
|
'verbosity', 'extra_vars'),
|
||||||
'classes': ('collapse',)}),
|
'classes': ('collapse',)}),
|
||||||
(_('Start/Cancel Job'), {'fields': ('start_job',)}),
|
(_('Start Job'), {'fields': ('start_job', 'ssh_password',
|
||||||
|
'sudo_password', 'ssh_key_unlock')}),
|
||||||
|
#(_('Cancel Job'), {'fields': ('cancel_job',)}),
|
||||||
#(_('Tags'), {'fields': ('tags',)}),
|
#(_('Tags'), {'fields': ('tags',)}),
|
||||||
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
|
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
|
||||||
'audit_trail',)}),
|
'audit_trail',)}),
|
||||||
(_('Job Status'), {'fields': ('status', 'get_result_stdout_display',
|
(_('Job Status'), {'fields': (('status', 'cancel_job'),
|
||||||
|
'get_result_stdout_display',
|
||||||
'get_result_stderr_display',
|
'get_result_stderr_display',
|
||||||
'get_result_traceback_display',
|
'get_result_traceback_display',
|
||||||
'celery_task_id')}),
|
'celery_task_id')}),
|
||||||
@@ -358,15 +361,16 @@ class JobAdmin(BaseModelAdmin):
|
|||||||
if not obj or not obj.pk or obj.status == 'new':
|
if not obj or not obj.pk or obj.status == 'new':
|
||||||
fsets = [fs for fs in fsets if
|
fsets = [fs for fs in fsets if
|
||||||
'creation_date' not in fs[1]['fields'] and
|
'creation_date' not in fs[1]['fields'] and
|
||||||
'status' not in fs[1]['fields']]
|
'celery_task_id' not in fs[1]['fields']]
|
||||||
elif obj and obj.pk and obj.status != 'new':
|
if not obj or (obj and obj.pk and obj.status != 'new'):
|
||||||
#print obj, obj.pk, obj.status
|
|
||||||
fsets = [fs for fs in fsets if 'start_job' not in fs[1]['fields']]
|
fsets = [fs for fs in fsets if 'start_job' not in fs[1]['fields']]
|
||||||
#for fs in fsets:
|
if not obj or (obj and obj.pk and obj.status not in ('pending', 'running')):
|
||||||
# # FIXME: Show start job on add view
|
for fs in fsets:
|
||||||
# if 'start_job' in fs[1]['fields']:
|
if 'celery_task_id' in fs[1]['fields']:
|
||||||
# fs[1]['fields'] = [x for x in fs[1]['fields']
|
fs[1]['fields'] = ('status', 'get_result_stdout_display',
|
||||||
# if x != 'start_job']
|
'get_result_stderr_display',
|
||||||
|
'get_result_traceback_display',
|
||||||
|
'celery_task_id')
|
||||||
return fsets
|
return fsets
|
||||||
|
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
@@ -406,6 +410,7 @@ class JobAdmin(BaseModelAdmin):
|
|||||||
get_result_traceback_display.short_description = _('Traceback')
|
get_result_traceback_display.short_description = _('Traceback')
|
||||||
get_result_traceback_display.allow_tags = True
|
get_result_traceback_display.allow_tags = True
|
||||||
|
|
||||||
|
|
||||||
# FIXME: Add the rest of the models...
|
# FIXME: Add the rest of the models...
|
||||||
|
|
||||||
admin.site.register(Organization, OrganizationAdmin)
|
admin.site.register(Organization, OrganizationAdmin)
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ class JobAdminForm(JobTemplateAdminForm):
|
|||||||
'''Custom admin form for creating Jobs.'''
|
'''Custom admin form for creating Jobs.'''
|
||||||
|
|
||||||
start_job = forms.BooleanField(initial=False, required=False)
|
start_job = forms.BooleanField(initial=False, required=False)
|
||||||
|
ssh_password = forms.CharField(label=_('SSH password'), required=False)
|
||||||
|
sudo_password = forms.CharField(required=False)
|
||||||
|
ssh_key_unlock = forms.CharField(label=_('SSH key passphrase'),
|
||||||
|
required=False)
|
||||||
|
cancel_job = forms.BooleanField(initial=False, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Job
|
model = Job
|
||||||
@@ -97,19 +102,45 @@ class JobAdminForm(JobTemplateAdminForm):
|
|||||||
super(JobAdminForm, self).__init__(*args, **kwargs)
|
super(JobAdminForm, self).__init__(*args, **kwargs)
|
||||||
if self.instance.pk and self.instance.status != 'new':
|
if self.instance.pk and self.instance.status != 'new':
|
||||||
self.fields.pop('playbook', None)
|
self.fields.pop('playbook', None)
|
||||||
|
if (not self.data or self.data.get('start_job', '')) and \
|
||||||
|
self.instance.credential and self.instance.status == 'new':
|
||||||
|
for field in self.instance.get_passwords_needed_to_start():
|
||||||
|
if field not in self.fields:
|
||||||
|
continue
|
||||||
|
self.fields[field].required = True
|
||||||
|
|
||||||
def clean_start_job(self):
|
def clean_start_job(self):
|
||||||
return self.cleaned_data.get('start_job', False)
|
return self.cleaned_data.get('start_job', False)
|
||||||
|
|
||||||
|
def clean_cancel_job(self):
|
||||||
|
return self.cleaned_data.get('cancel_job', False)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.instance.credential and self.instance.status == 'new':
|
||||||
|
for field in self.instance.get_passwords_needed_to_start():
|
||||||
|
if field in self.fields:
|
||||||
|
self.fields[field].required = True
|
||||||
|
return super(JobAdminForm, self).clean()
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
instance = super(JobAdminForm, self).save(commit)
|
instance = super(JobAdminForm, self).save(commit)
|
||||||
save_m2m = getattr(self, 'save_m2m', lambda: None)
|
save_m2m = getattr(self, 'save_m2m', lambda: None)
|
||||||
should_start = bool(self.cleaned_data.get('start_job', '') and
|
should_start = bool(self.cleaned_data.get('start_job', '') and
|
||||||
instance.status == 'new')
|
instance.status == 'new')
|
||||||
|
start_opts = {}
|
||||||
|
for field in ('ssh_password', 'sudo_password', 'ssh_key_unlock'):
|
||||||
|
value = self.cleaned_data.get(field, '')
|
||||||
|
if value:
|
||||||
|
start_opts[field] = value
|
||||||
|
#print 'should_start', should_start
|
||||||
|
should_cancel = bool(self.cleaned_data.get('cancel_job', '') and
|
||||||
|
instance.status in ('new', 'pending'))
|
||||||
def new_save_m2m():
|
def new_save_m2m():
|
||||||
save_m2m()
|
save_m2m()
|
||||||
if should_start:
|
if should_start:
|
||||||
instance.start()
|
instance.start(**start_opts)
|
||||||
|
if should_cancel:
|
||||||
|
instance.cancel()
|
||||||
if commit:
|
if commit:
|
||||||
new_save_m2m()
|
new_save_m2m()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -901,8 +901,7 @@ class JobTemplate(CommonModel):
|
|||||||
'''
|
'''
|
||||||
Create a new job based on this template.
|
Create a new job based on this template.
|
||||||
'''
|
'''
|
||||||
start_job = kwargs.pop('start', False)
|
save_job = kwargs.pop('save', True)
|
||||||
save_job = kwargs.pop('save', True) or start_job # Start implies save.
|
|
||||||
kwargs['job_template'] = self
|
kwargs['job_template'] = self
|
||||||
kwargs.setdefault('name', '%s %s' % (self.name, now().isoformat()))
|
kwargs.setdefault('name', '%s %s' % (self.name, now().isoformat()))
|
||||||
kwargs.setdefault('description', self.description)
|
kwargs.setdefault('description', self.description)
|
||||||
@@ -919,8 +918,6 @@ class JobTemplate(CommonModel):
|
|||||||
job = Job(**kwargs)
|
job = Job(**kwargs)
|
||||||
if save_job:
|
if save_job:
|
||||||
job.save()
|
job.save()
|
||||||
if start_job:
|
|
||||||
job.start()
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
# project has one default playbook but really should have a list of playbooks and flags ...
|
# project has one default playbook but really should have a list of playbooks and flags ...
|
||||||
@@ -1137,14 +1134,22 @@ class Job(CommonModel):
|
|||||||
except TaskMeta.DoesNotExist:
|
except TaskMeta.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_passwords_needed_to_start(self):
|
||||||
|
'''Return list of password field names needed to start the job.'''
|
||||||
|
needed = []
|
||||||
|
for field in ('ssh_password', 'sudo_password', 'ssh_key_unlock'):
|
||||||
|
if self.credential and getattr(self.credential, 'needs_%s' % field):
|
||||||
|
needed.append(field)
|
||||||
|
return needed
|
||||||
|
|
||||||
def start(self, **kwargs):
|
def start(self, **kwargs):
|
||||||
from lib.main.tasks import RunJob
|
from lib.main.tasks import RunJob
|
||||||
if self.status != 'new':
|
if self.status != 'new':
|
||||||
return False
|
return False
|
||||||
|
needed = self.get_passwords_needed_to_start()
|
||||||
#username = kwargs.get('username', self.username)
|
opts = dict([(field, kwargs.get(field, '')) for field in needed])
|
||||||
|
if not all(opts.values()):
|
||||||
opts = {}
|
return False
|
||||||
self.status = 'pending'
|
self.status = 'pending'
|
||||||
self.save(update_fields=['status'])
|
self.save(update_fields=['status'])
|
||||||
task_result = RunJob().delay(self.pk, **opts)
|
task_result = RunJob().delay(self.pk, **opts)
|
||||||
|
|||||||
@@ -55,6 +55,33 @@ class RunJob(Task):
|
|||||||
'''
|
'''
|
||||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), *args))
|
return os.path.abspath(os.path.join(os.path.dirname(__file__), *args))
|
||||||
|
|
||||||
|
def build_ssh_key_path(self, job, **kwargs):
|
||||||
|
'''
|
||||||
|
Create a temporary file containing the SSH private key.
|
||||||
|
'''
|
||||||
|
creds = job.credential
|
||||||
|
if creds and creds.ssh_key_data:
|
||||||
|
handle, path = tempfile.mkstemp()
|
||||||
|
f = os.fdopen(handle, 'w')
|
||||||
|
f.write(creds.ssh_key_data)
|
||||||
|
f.close()
|
||||||
|
return path
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def build_passwords(self, job, **kwargs):
|
||||||
|
'''
|
||||||
|
Build a dictionary of passwords for SSH private key, SSH user and sudo.
|
||||||
|
'''
|
||||||
|
passwords = {}
|
||||||
|
creds = job.credential
|
||||||
|
if creds:
|
||||||
|
for field in ('ssh_key_unlock', 'ssh_password', 'sudo_password'):
|
||||||
|
value = kwargs.get(field, getattr(creds, field))
|
||||||
|
if value not in ('', 'ASK'):
|
||||||
|
passwords[field] = value
|
||||||
|
return passwords
|
||||||
|
|
||||||
def build_env(self, job, **kwargs):
|
def build_env(self, job, **kwargs):
|
||||||
'''
|
'''
|
||||||
Build environment dictionary for ansible-playbook.
|
Build environment dictionary for ansible-playbook.
|
||||||
@@ -80,18 +107,25 @@ class RunJob(Task):
|
|||||||
optionally using ssh-agent for public/private key authentication.
|
optionally using ssh-agent for public/private key authentication.
|
||||||
'''
|
'''
|
||||||
creds = job.credential
|
creds = job.credential
|
||||||
use_ssh_agent = False
|
ssh_username, sudo_username = '', ''
|
||||||
if creds:
|
if creds:
|
||||||
username = creds.ssh_username
|
ssh_username = kwargs.get('ssh_username', creds.ssh_username)
|
||||||
sudo_username = creds.sudo_username
|
sudo_username = kwargs.get('sudo_username', creds.sudo_username)
|
||||||
# FIXME: Do something with creds.
|
ssh_username = ssh_username or 'root'
|
||||||
|
sudo_username = sudo_username or 'root'
|
||||||
inventory_script = self.get_path_to('management', 'commands',
|
inventory_script = self.get_path_to('management', 'commands',
|
||||||
'acom_inventory.py')
|
'acom_inventory.py')
|
||||||
args = ['ansible-playbook', '-i', inventory_script]
|
args = ['ansible-playbook', '-i', inventory_script]
|
||||||
if job.job_type == 'check':
|
if job.job_type == 'check':
|
||||||
args.append('--check')
|
args.append('--check')
|
||||||
|
args.append('--user=%s' % ssh_username)
|
||||||
|
if 'ssh_password' in kwargs.get('passwords', {}):
|
||||||
|
args.append('--ask-pass')
|
||||||
if job.use_sudo:
|
if job.use_sudo:
|
||||||
args.append('--sudo')
|
args.append('--sudo')
|
||||||
|
args.append('--sudo-user=%s' % sudo_username)
|
||||||
|
if 'sudo_password' in kwargs.get('passwords', {}):
|
||||||
|
args.append('--ask-sudo-pass')
|
||||||
if job.forks: # FIXME: Max limit?
|
if job.forks: # FIXME: Max limit?
|
||||||
args.append('--forks=%d' % job.forks)
|
args.append('--forks=%d' % job.forks)
|
||||||
if job.limit:
|
if job.limit:
|
||||||
@@ -102,22 +136,16 @@ class RunJob(Task):
|
|||||||
# FIXME: escaping!
|
# FIXME: escaping!
|
||||||
extra_vars = ' '.join(['%s=%s' % (str(k), str(v)) for k,v in
|
extra_vars = ' '.join(['%s=%s' % (str(k), str(v)) for k,v in
|
||||||
job.extra_vars.items()])
|
job.extra_vars.items()])
|
||||||
args.append('-e', extra_vars)
|
args.append('--extra-vars=%s' % extra_vars)
|
||||||
args.append(job.playbook) # relative path to project.local_path
|
args.append(job.playbook) # relative path to project.local_path
|
||||||
if use_ssh_agent:
|
ssh_key_path = kwargs.get('ssh_key_path', '')
|
||||||
key_path = 'myrsa' # FIXME
|
if ssh_key_path:
|
||||||
cmd = '; '.join([subprocess.list2cmdline(['ssh-add', keypath]),
|
cmd = '; '.join([subprocess.list2cmdline(['ssh-add', ssh_key_path]),
|
||||||
subprocess.list2cmdline(args)])
|
subprocess.list2cmdline(args)])
|
||||||
return ['ssh-agent', 'sh', '-c', cmd]
|
return ['ssh-agent', 'sh', '-c', cmd]
|
||||||
else:
|
else:
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def build_passwords(self, job, **kwargs):
|
|
||||||
'''
|
|
||||||
Build a dictionary of passwords for SSH private key, SSH user and sudo.
|
|
||||||
'''
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def capture_subprocess_output(self, proc, timeout=1.0):
|
def capture_subprocess_output(self, proc, timeout=1.0):
|
||||||
'''
|
'''
|
||||||
Capture stdout/stderr from the given process until the timeout expires.
|
Capture stdout/stderr from the given process until the timeout expires.
|
||||||
@@ -195,6 +223,7 @@ class RunJob(Task):
|
|||||||
status, stdout, stderr = 'error', '', ''
|
status, stdout, stderr = 'error', '', ''
|
||||||
logfile = cStringIO.StringIO()
|
logfile = cStringIO.StringIO()
|
||||||
logfile_pos = logfile.tell()
|
logfile_pos = logfile.tell()
|
||||||
|
print 'ARGS:', repr(args)
|
||||||
child = pexpect.spawn(args[0], args[1:], cwd=cwd, env=env)
|
child = pexpect.spawn(args[0], args[1:], cwd=cwd, env=env)
|
||||||
child.logfile_read = logfile
|
child.logfile_read = logfile
|
||||||
job_canceled = False
|
job_canceled = False
|
||||||
@@ -209,7 +238,7 @@ class RunJob(Task):
|
|||||||
]
|
]
|
||||||
result_id = child.expect(expect_list, timeout=2)
|
result_id = child.expect(expect_list, timeout=2)
|
||||||
if result_id == 0:
|
if result_id == 0:
|
||||||
child.sendline(passwords.get('ssh_unlock_key', ''))
|
child.sendline(passwords.get('ssh_key_unlock', ''))
|
||||||
elif result_id == 1:
|
elif result_id == 1:
|
||||||
child.sendline('')
|
child.sendline('')
|
||||||
elif result_id == 2:
|
elif result_id == 2:
|
||||||
@@ -237,17 +266,24 @@ class RunJob(Task):
|
|||||||
Run the job using ansible-playbook and capture its output.
|
Run the job using ansible-playbook and capture its output.
|
||||||
'''
|
'''
|
||||||
job = self.update_job(job_pk, status='running')
|
job = self.update_job(job_pk, status='running')
|
||||||
|
status, stdout, stderr, tb = 'error', '', '', ''
|
||||||
try:
|
try:
|
||||||
status, stdout, stderr, tb = 'error', '', '', ''
|
kwargs['ssh_key_path'] = self.build_ssh_key_path(job, **kwargs)
|
||||||
|
kwargs['passwords'] = self.build_passwords(job, **kwargs)
|
||||||
args = self.build_args(job, **kwargs)
|
args = self.build_args(job, **kwargs)
|
||||||
cwd = job.project.local_path
|
cwd = job.project.local_path
|
||||||
env = self.build_env(job, **kwargs)
|
env = self.build_env(job, **kwargs)
|
||||||
passwords = self.build_passwords(job, **kwargs)
|
|
||||||
#status, stdout, stderr = self.run_subprocess(job_pk, args, cwd,
|
#status, stdout, stderr = self.run_subprocess(job_pk, args, cwd,
|
||||||
# env, passwords)
|
# env, passwords)
|
||||||
status, stdout, stderr = self.run_pexpect(job_pk, args, cwd, env,
|
status, stdout, stderr = self.run_pexpect(job_pk, args, cwd, env,
|
||||||
passwords)
|
kwargs['passwords'])
|
||||||
except Exception:
|
except Exception:
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
|
finally:
|
||||||
|
if kwargs.get('ssh_key_path', ''):
|
||||||
|
try:
|
||||||
|
os.remove(kwargs['ssh_key_path'])
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
self.update_job(job_pk, status=status, result_stdout=stdout,
|
self.update_job(job_pk, status=status, result_stdout=stdout,
|
||||||
result_stderr=stderr, result_traceback=tb)
|
result_stderr=stderr, result_traceback=tb)
|
||||||
|
|||||||
Reference in New Issue
Block a user