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', (_('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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)