Jobs updates to start/cancel and prompt for passwords via the admin.

This commit is contained in:
Chris Church 2013-04-25 01:11:55 -04:00
parent b2c4ca6ece
commit 52d31d105d
4 changed files with 114 additions and 37 deletions

View File

@ -327,11 +327,14 @@ class JobAdmin(BaseModelAdmin):
(_('More Options'), {'fields': ('use_sudo', 'forks', 'limit',
'verbosity', 'extra_vars'),
'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',)}),
(_('Audit Trail'), {'fields': ('creation_date', 'created_by',
'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_traceback_display',
'celery_task_id')}),
@ -358,15 +361,16 @@ class JobAdmin(BaseModelAdmin):
if not obj or not obj.pk or obj.status == 'new':
fsets = [fs for fs in fsets if
'creation_date' not in fs[1]['fields'] and
'status' not in fs[1]['fields']]
elif obj and obj.pk and obj.status != 'new':
#print obj, obj.pk, obj.status
'celery_task_id' not in fs[1]['fields']]
if not obj or (obj and obj.pk and obj.status != 'new'):
fsets = [fs for fs in fsets if 'start_job' not in fs[1]['fields']]
#for fs in fsets:
# # FIXME: Show start job on add view
# if 'start_job' in fs[1]['fields']:
# fs[1]['fields'] = [x for x in fs[1]['fields']
# if x != 'start_job']
if not obj or (obj and obj.pk and obj.status not in ('pending', 'running')):
for fs in fsets:
if 'celery_task_id' in fs[1]['fields']:
fs[1]['fields'] = ('status', 'get_result_stdout_display',
'get_result_stderr_display',
'get_result_traceback_display',
'celery_task_id')
return fsets
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.allow_tags = True
# FIXME: Add the rest of the models...
admin.site.register(Organization, OrganizationAdmin)

View File

@ -89,6 +89,11 @@ class JobAdminForm(JobTemplateAdminForm):
'''Custom admin form for creating Jobs.'''
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:
model = Job
@ -97,19 +102,45 @@ class JobAdminForm(JobTemplateAdminForm):
super(JobAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk and self.instance.status != 'new':
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):
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):
instance = super(JobAdminForm, self).save(commit)
save_m2m = getattr(self, 'save_m2m', lambda: None)
should_start = bool(self.cleaned_data.get('start_job', '') and
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():
save_m2m()
if should_start:
instance.start()
instance.start(**start_opts)
if should_cancel:
instance.cancel()
if commit:
new_save_m2m()
else:

View File

@ -901,8 +901,7 @@ class JobTemplate(CommonModel):
'''
Create a new job based on this template.
'''
start_job = kwargs.pop('start', False)
save_job = kwargs.pop('save', True) or start_job # Start implies save.
save_job = kwargs.pop('save', True)
kwargs['job_template'] = self
kwargs.setdefault('name', '%s %s' % (self.name, now().isoformat()))
kwargs.setdefault('description', self.description)
@ -919,8 +918,6 @@ class JobTemplate(CommonModel):
job = Job(**kwargs)
if save_job:
job.save()
if start_job:
job.start()
return job
# 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:
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):
from lib.main.tasks import RunJob
if self.status != 'new':
return False
#username = kwargs.get('username', self.username)
opts = {}
needed = self.get_passwords_needed_to_start()
opts = dict([(field, kwargs.get(field, '')) for field in needed])
if not all(opts.values()):
return False
self.status = 'pending'
self.save(update_fields=['status'])
task_result = RunJob().delay(self.pk, **opts)

View File

@ -55,6 +55,33 @@ class RunJob(Task):
'''
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):
'''
Build environment dictionary for ansible-playbook.
@ -80,18 +107,25 @@ class RunJob(Task):
optionally using ssh-agent for public/private key authentication.
'''
creds = job.credential
use_ssh_agent = False
ssh_username, sudo_username = '', ''
if creds:
username = creds.ssh_username
sudo_username = creds.sudo_username
# FIXME: Do something with creds.
ssh_username = kwargs.get('ssh_username', creds.ssh_username)
sudo_username = kwargs.get('sudo_username', creds.sudo_username)
ssh_username = ssh_username or 'root'
sudo_username = sudo_username or 'root'
inventory_script = self.get_path_to('management', 'commands',
'acom_inventory.py')
args = ['ansible-playbook', '-i', inventory_script]
if job.job_type == '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:
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?
args.append('--forks=%d' % job.forks)
if job.limit:
@ -102,22 +136,16 @@ class RunJob(Task):
# FIXME: escaping!
extra_vars = ' '.join(['%s=%s' % (str(k), str(v)) for k,v in
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
if use_ssh_agent:
key_path = 'myrsa' # FIXME
cmd = '; '.join([subprocess.list2cmdline(['ssh-add', keypath]),
ssh_key_path = kwargs.get('ssh_key_path', '')
if ssh_key_path:
cmd = '; '.join([subprocess.list2cmdline(['ssh-add', ssh_key_path]),
subprocess.list2cmdline(args)])
return ['ssh-agent', 'sh', '-c', cmd]
else:
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):
'''
Capture stdout/stderr from the given process until the timeout expires.
@ -195,6 +223,7 @@ class RunJob(Task):
status, stdout, stderr = 'error', '', ''
logfile = cStringIO.StringIO()
logfile_pos = logfile.tell()
print 'ARGS:', repr(args)
child = pexpect.spawn(args[0], args[1:], cwd=cwd, env=env)
child.logfile_read = logfile
job_canceled = False
@ -209,7 +238,7 @@ class RunJob(Task):
]
result_id = child.expect(expect_list, timeout=2)
if result_id == 0:
child.sendline(passwords.get('ssh_unlock_key', ''))
child.sendline(passwords.get('ssh_key_unlock', ''))
elif result_id == 1:
child.sendline('')
elif result_id == 2:
@ -237,17 +266,24 @@ class RunJob(Task):
Run the job using ansible-playbook and capture its output.
'''
job = self.update_job(job_pk, status='running')
status, stdout, stderr, tb = 'error', '', '', ''
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)
cwd = job.project.local_path
env = self.build_env(job, **kwargs)
passwords = self.build_passwords(job, **kwargs)
#status, stdout, stderr = self.run_subprocess(job_pk, args, cwd,
# env, passwords)
status, stdout, stderr = self.run_pexpect(job_pk, args, cwd, env,
passwords)
kwargs['passwords'])
except Exception:
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,
result_stderr=stderr, result_traceback=tb)