2013-04-02 00:04:27 +04:00
# Copyright (c) 2013 AnsibleWorks, Inc.
2013-03-24 02:43:11 +04:00
#
2013-04-09 09:05:55 +04:00
# This file is part of Ansible Commander.
#
2013-03-24 02:43:11 +04:00
# Ansible Commander is free software: you can redistribute it and/or modify
2013-04-09 09:05:55 +04:00
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
2013-03-24 02:43:11 +04:00
#
2013-04-09 09:05:55 +04:00
# Ansible Commander is distributed in the hope that it will be useful,
2013-03-24 02:43:11 +04:00
# but WITHOUT ANY WARRANTY; without even the implied warranty of
2013-04-09 09:05:55 +04:00
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible Commander. If not, see <http://www.gnu.org/licenses/>.
2013-03-24 02:43:11 +04:00
2013-04-24 19:35:30 +04:00
import cStringIO
2013-04-24 00:21:29 +04:00
import logging
2013-03-23 02:55:10 +04:00
import os
2013-04-24 00:21:29 +04:00
import select
2013-03-23 02:55:10 +04:00
import subprocess
2013-04-27 02:24:12 +04:00
import tempfile
2013-04-24 00:21:29 +04:00
import time
2013-04-05 00:53:20 +04:00
import traceback
2013-04-24 19:35:30 +04:00
from celery import Task
2013-04-01 01:25:18 +04:00
from django . conf import settings
2013-04-24 19:35:30 +04:00
import pexpect
2013-03-15 00:11:14 +04:00
from lib . main . models import *
2013-04-24 19:35:30 +04:00
__all__ = [ ' RunJob ' ]
logger = logging . getLogger ( ' lib.main.tasks ' )
class RunJob ( Task ) :
'''
Celery task to run a job using ansible - playbook .
'''
name = ' run_job '
def update_job ( self , job_pk , * * job_updates ) :
'''
Reload Job from database and update the given fields .
'''
job = Job . objects . get ( pk = job_pk )
if job_updates :
2013-05-09 01:41:10 +04:00
update_fields = [ ]
2013-04-24 19:35:30 +04:00
for field , value in job_updates . items ( ) :
setattr ( job , field , value )
2013-05-09 01:41:10 +04:00
update_fields . append ( field )
if field == ' status ' :
update_fields . append ( ' failed ' )
job . save ( update_fields = update_fields )
2013-04-24 19:35:30 +04:00
return job
def get_path_to ( self , * args ) :
'''
Return absolute path relative to this file .
'''
return os . path . abspath ( os . path . join ( os . path . dirname ( __file__ ) , * args ) )
2013-04-25 09:11:55 +04:00
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 :
2013-04-27 02:24:12 +04:00
# FIXME: File permissions?
2013-04-25 09:11:55 +04:00
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
2013-04-24 19:35:30 +04:00
def build_env ( self , job , * * kwargs ) :
'''
Build environment dictionary for ansible - playbook .
'''
plugin_dir = self . get_path_to ( ' .. ' , ' plugins ' , ' callback ' )
callback_script = self . get_path_to ( ' management ' , ' commands ' ,
' acom_callback_event.py ' )
2013-04-05 00:53:20 +04:00
env = dict ( os . environ . items ( ) )
2013-04-09 08:55:25 +04:00
# question: when running over CLI, generate a random ID or grab next, etc?
2013-04-18 02:59:21 +04:00
# answer: TBD
env [ ' ACOM_JOB_ID ' ] = str ( job . pk )
env [ ' ACOM_INVENTORY_ID ' ] = str ( job . inventory . pk )
2013-04-05 00:53:20 +04:00
env [ ' ANSIBLE_CALLBACK_PLUGINS ' ] = plugin_dir
env [ ' ACOM_CALLBACK_EVENT_SCRIPT ' ] = callback_script
if hasattr ( settings , ' ANSIBLE_TRANSPORT ' ) :
env [ ' ANSIBLE_TRANSPORT ' ] = getattr ( settings , ' ANSIBLE_TRANSPORT ' )
2013-04-24 19:35:30 +04:00
env [ ' ANSIBLE_NOCOLOR ' ] = ' 1 ' # Prevent output of escape sequences.
return env
def build_args ( self , job , * * kwargs ) :
'''
Build command line argument list for running ansible - playbook ,
optionally using ssh - agent for public / private key authentication .
'''
2013-04-24 00:21:29 +04:00
creds = job . credential
2013-04-25 09:11:55 +04:00
ssh_username , sudo_username = ' ' , ' '
2013-04-24 19:35:30 +04:00
if creds :
2013-04-25 09:11:55 +04:00
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 '
2013-04-24 19:35:30 +04:00
inventory_script = self . get_path_to ( ' management ' , ' commands ' ,
' acom_inventory.py ' )
args = [ ' ansible-playbook ' , ' -i ' , inventory_script ]
2013-04-18 02:59:21 +04:00
if job . job_type == ' check ' :
2013-04-24 19:35:30 +04:00
args . append ( ' --check ' )
2013-05-09 01:41:10 +04:00
args . extend ( [ ' -u ' , ssh_username ] )
2013-04-25 09:11:55 +04:00
if ' ssh_password ' in kwargs . get ( ' passwords ' , { } ) :
args . append ( ' --ask-pass ' )
2013-05-09 01:41:10 +04:00
args . extend ( [ ' -U ' , sudo_username ] )
2013-04-25 09:11:55 +04:00
if ' sudo_password ' in kwargs . get ( ' passwords ' , { } ) :
args . append ( ' --ask-sudo-pass ' )
2013-04-24 00:21:29 +04:00
if job . forks : # FIXME: Max limit?
2013-04-24 19:35:30 +04:00
args . append ( ' --forks= %d ' % job . forks )
2013-04-24 00:21:29 +04:00
if job . limit :
2013-05-09 01:41:10 +04:00
args . extend ( [ ' -l ' , job . limit ] )
2013-04-24 00:21:29 +04:00
if job . verbosity :
2013-04-24 19:35:30 +04:00
args . append ( ' - %s ' % ( ' v ' * min ( 3 , job . verbosity ) ) )
2013-04-24 00:21:29 +04:00
if job . extra_vars :
2013-05-09 01:41:10 +04:00
args . extend ( [ ' -e ' , job . extra_vars ] )
2013-04-24 19:35:30 +04:00
args . append ( job . playbook ) # relative path to project.local_path
2013-04-25 09:11:55 +04:00
ssh_key_path = kwargs . get ( ' ssh_key_path ' , ' ' )
if ssh_key_path :
2013-04-27 02:24:12 +04:00
cmd = ' ' . join ( [ subprocess . list2cmdline ( [ ' ssh-add ' , ssh_key_path ] ) ,
' && ' , subprocess . list2cmdline ( args ) ] )
args = [ ' ssh-agent ' , ' sh ' , ' -c ' , cmd ]
return args
2013-04-24 19:35:30 +04:00
def run_pexpect ( self , job_pk , args , cwd , env , passwords ) :
'''
Run the job using pexpect to capture output and provide passwords when
requested .
'''
2013-05-09 01:41:10 +04:00
status , stdout = ' error ' , ' '
2013-04-24 19:35:30 +04:00
logfile = cStringIO . StringIO ( )
logfile_pos = logfile . tell ( )
child = pexpect . spawn ( args [ 0 ] , args [ 1 : ] , cwd = cwd , env = env )
child . logfile_read = logfile
job_canceled = False
while child . isalive ( ) :
expect_list = [
r ' Enter passphrase for .*: ' ,
r ' Bad passphrase, try again for .*: ' ,
r ' sudo password.*: ' ,
r ' SSH password: ' ,
pexpect . TIMEOUT ,
pexpect . EOF ,
]
result_id = child . expect ( expect_list , timeout = 2 )
if result_id == 0 :
2013-04-25 09:11:55 +04:00
child . sendline ( passwords . get ( ' ssh_key_unlock ' , ' ' ) )
2013-04-24 19:35:30 +04:00
elif result_id == 1 :
child . sendline ( ' ' )
elif result_id == 2 :
child . sendline ( passwords . get ( ' sudo_password ' , ' ' ) )
elif result_id == 3 :
child . sendline ( passwords . get ( ' ssh_password ' , ' ' ) )
job_updates = { }
if logfile_pos != logfile . tell ( ) :
job_updates [ ' result_stdout ' ] = logfile . getvalue ( )
job = self . update_job ( job_pk , * * job_updates )
if job . cancel_flag :
child . close ( True )
job_canceled = True
if job_canceled :
status = ' canceled '
elif child . exitstatus == 0 :
status = ' successful '
else :
status = ' failed '
stdout = logfile . getvalue ( )
2013-05-09 01:41:10 +04:00
return status , stdout
2013-04-24 19:35:30 +04:00
def run ( self , job_pk , * * kwargs ) :
'''
Run the job using ansible - playbook and capture its output .
'''
job = self . update_job ( job_pk , status = ' running ' )
2013-05-09 01:41:10 +04:00
status , stdout , tb = ' error ' , ' ' , ' '
2013-04-24 19:35:30 +04:00
try :
2013-04-25 09:11:55 +04:00
kwargs [ ' ssh_key_path ' ] = self . build_ssh_key_path ( job , * * kwargs )
kwargs [ ' passwords ' ] = self . build_passwords ( job , * * kwargs )
2013-04-24 19:35:30 +04:00
args = self . build_args ( job , * * kwargs )
2013-05-10 08:44:13 +04:00
cwd = job . project . get_project_path ( )
if not cwd :
raise RuntimeError ( ' project local_path %s cannot be found ' %
project . local_path )
2013-04-24 19:35:30 +04:00
env = self . build_env ( job , * * kwargs )
2013-05-09 01:41:10 +04:00
status , stdout = self . run_pexpect ( job_pk , args , cwd , env ,
kwargs [ ' passwords ' ] )
2013-04-24 19:35:30 +04:00
except Exception :
tb = traceback . format_exc ( )
2013-04-25 09:11:55 +04:00
finally :
if kwargs . get ( ' ssh_key_path ' , ' ' ) :
try :
os . remove ( kwargs [ ' ssh_key_path ' ] )
except IOError :
pass
2013-04-24 19:35:30 +04:00
self . update_job ( job_pk , status = status , result_stdout = stdout ,
2013-05-09 01:41:10 +04:00
result_traceback = tb )