From 52d31d105df1ddd2fee1a6e6ce9cd7b7d5410cb7 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 25 Apr 2013 01:11:55 -0400 Subject: [PATCH] Jobs updates to start/cancel and prompt for passwords via the admin. --- lib/main/admin.py | 25 +++++++------ lib/main/forms.py | 33 ++++++++++++++++- lib/main/models/__init__.py | 21 ++++++----- lib/main/tasks.py | 72 +++++++++++++++++++++++++++---------- 4 files changed, 114 insertions(+), 37 deletions(-) diff --git a/lib/main/admin.py b/lib/main/admin.py index 74908bff10..14c54a5d94 100644 --- a/lib/main/admin.py +++ b/lib/main/admin.py @@ -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) diff --git a/lib/main/forms.py b/lib/main/forms.py index bd60c31938..c90c14c462 100644 --- a/lib/main/forms.py +++ b/lib/main/forms.py @@ -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: diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index 41bbe0c76d..cfc2a15dc4 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -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) diff --git a/lib/main/tasks.py b/lib/main/tasks.py index c3f3c24b78..fdcc4287b4 100644 --- a/lib/main/tasks.py +++ b/lib/main/tasks.py @@ -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)