Package actions :: Module script
[hide private]
[frames] | no frames]

Source Code for Module actions.script

  1  # 
  2  # Copyright (c) 2008--2017 Red Hat, Inc. 
  3  # 
  4  # This software is licensed to you under the GNU General Public License, 
  5  # version 2 (GPLv2). There is NO WARRANTY for this software, express or 
  6  # implied, including the implied warranties of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 
  8  # along with this software; if not, see 
  9  # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 
 10  # 
 11  # Red Hat trademarks are not licensed under GPLv2. No permission is 
 12  # granted to use or replicate Red Hat trademarks that are incorporated 
 13  # in this software or its documentation. 
 14  # 
 15   
 16  import os 
 17  import sys 
 18  import pwd 
 19  import grp 
 20  import time 
 21  import select 
 22  import signal 
 23  import tempfile 
 24  import base64 
 25   
 26  try: 
 27      MAXFD = os.sysconf("SC_OPEN_MAX") 
 28  except: 
 29      MAXFD = 256 
 30   
 31   
 32  # this is ugly, hopefully it will be natively supported in up2date 
 33  from rhn.actions.configfiles import _local_permission_check, _perm_error 
 34  from config_common import local_config 
 35  from config_common.rhn_log import set_logfile, log_to_file 
 36   
 37  from up2date_client import config 
 38   
 39   
 40  # this is a list of the methods that get exported by a module 
 41  __rhnexport__ = [ 
 42      'run', 
 43      ] 
 44   
 45  # action version we understand 
 46  ACTION_VERSION = 2 
 47   
 48   
49 -def _create_script_file(script, uid=None, gid=None):
50 51 storageDir = tempfile.gettempdir() 52 script_path = os.path.join(storageDir, 'rhn-remote-script') 53 54 # Loop a couple of times to try to get rid of race conditions 55 for i in range(2): 56 try: 57 fd = os.open(script_path, os.O_RDWR | os.O_CREAT | os.O_EXCL, int("0700", 8)) 58 # If this succeeds, break out the loop 59 break 60 except OSError: 61 e = sys.exc_info()[1] 62 if e.errno != 17: # File exists 63 raise 64 # File does exist, try to remove it 65 try: 66 os.unlink(script_path) 67 except OSError: 68 e = sys.exc_info()[1] 69 if e.errno != 2: # No such file or directory 70 raise 71 else: 72 # Tried a couple of times, failed; bail out raising the latest error 73 raise 74 sf = os.fdopen(fd, 'wb') 75 sf.write(script.encode("utf-8")) 76 sf.close() 77 78 if uid and gid: 79 os.chown(script_path, uid, gid) 80 81 return script_path
82 83 # Make sure the dir-path to a file exists
84 -def _create_path(fpath):
85 d = os.path.dirname(fpath) 86 if d and not os.path.exists(d): 87 os.makedirs(d, int("0700", 8)) 88 return os.path.exists(d)
89
90 -def run(action_id, params, cache_only=None):
91 92 cfg = config.initUp2dateConfig() 93 local_config.init('rhncfg-client', defaults=dict(cfg.items())) 94 95 tempfile.tempdir = local_config.get('script_tmp_dir') 96 97 logfile_name = local_config.get('script_log_file') 98 log_output = local_config.get('script_log_file_enable') 99 100 if log_output: 101 # If we're going to log, make sure we can create the logfile 102 _create_path(logfile_name) 103 104 if cache_only: 105 return (0, "no-ops for caching", {}) 106 107 action_type = 'script.run' 108 if not _local_permission_check(action_type): 109 return _perm_error(action_type) 110 111 112 extras = {'output':''} 113 script = params.get('script') 114 if not script: 115 return (1, "No script to execute", {}) 116 117 username = params.get('username') 118 groupname = params.get('groupname') 119 120 if not username: 121 return (1, "No username given to execute script as", {}) 122 123 if not groupname: 124 return (1, "No groupname given to execute script as", {}) 125 126 timeout = params.get('timeout') 127 128 if timeout: 129 try: 130 timeout = int(timeout) 131 except ValueError: 132 return (1, "Invalid timeout value", {}) 133 else: 134 timeout = None 135 136 db_now = params.get('now') 137 if not db_now: 138 return (1, "'now' argument missing", {}) 139 db_now = time.mktime(time.strptime(db_now, "%Y-%m-%d %H:%M:%S")) 140 141 now = time.time() 142 process_start = None 143 process_end = None 144 145 child_pid = None 146 147 # determine uid/ugid for script ownership, uid also used for setuid... 148 try: 149 user_record = pwd.getpwnam(username) 150 except KeyError: 151 return 1, "No such user %s" % username, extras 152 153 uid = user_record[2] 154 ugid = user_record[3] 155 156 157 # create the script on disk 158 try: 159 script_path = _create_script_file(script, uid=uid, gid=ugid) 160 except OSError: 161 e = sys.exc_info()[1] 162 return 1, "Problem creating script file: %s" % e, extras 163 164 # determine gid to run script as 165 try: 166 group_record = grp.getgrnam(groupname) 167 except KeyError: 168 return 1, "No such group %s" % groupname, extras 169 170 run_as_gid = group_record[2] 171 172 173 # create some pipes to communicate w/ the child process 174 (pipe_read, pipe_write) = os.pipe() 175 176 process_start = time.time() 177 child_pid = os.fork() 178 179 if not child_pid: 180 # Parent doesn't write to child, so close that part 181 os.close(pipe_read) 182 183 # Redirect both stdout and stderr to the pipe 184 os.dup2(pipe_write, sys.stdout.fileno()) 185 os.dup2(pipe_write, sys.stderr.fileno()) 186 187 # Close unnecessary file descriptors (including pipe since it's duped) 188 for i in range(3, MAXFD): 189 try: 190 os.close(i) 191 except: 192 pass 193 194 # all scripts initial working directory will be / 195 # puts burden on script writer to ensure cwd is correct within the 196 # script 197 os.chdir('/') 198 199 # the child process gets the desired uid/gid 200 os.setgid(run_as_gid) 201 groups=[g.gr_gid for g in grp.getgrall() if username in g.gr_mem or username in g.gr_name] 202 os.setgroups(groups) 203 os.setuid(uid) 204 205 # give this its own process group (which happens to be equal to its 206 # pid) 207 os.setpgrp() 208 209 clean_env = {"PATH": "/sbin:/bin:/usr/sbin:/usr/bin", "TERM": "xterm"} 210 # Finally, exec the script 211 try: 212 os.umask(int("022", 8)) 213 os.execve(script_path, [script_path, ], clean_env) 214 finally: 215 # This code can be reached only when script_path can not be 216 # executed as otherwise execv never returns. 217 # (The umask syscall always succeeds.) 218 os._exit(1) 219 220 # Parent doesn't write to child, so close that part 221 os.close(pipe_write) 222 223 output = None 224 timed_out = None 225 226 out_stream = open('/var/lib/up2date/action.%s' % str(action_id), 'ab+', 0) 227 228 while 1: 229 select_wait = None 230 231 if timeout: 232 elapsed = time.time() - process_start 233 234 if elapsed >= timeout: 235 timed_out = 1 236 # Send TERM to all processes in the child's process group 237 # Send KILL after that, just to make sure the child died 238 os.kill(-child_pid, signal.SIGTERM) 239 time.sleep(2) 240 os.kill(-child_pid, signal.SIGKILL) 241 break 242 243 select_wait = timeout - elapsed 244 245 try: 246 input_fds, output_fds, error_fds = select.select([pipe_read], [], [], select_wait) 247 except select.error: 248 return 255, "Termination signal occurred during execution.", {} 249 250 if error_fds: 251 # when would this happen? 252 os.close(pipe_read) 253 return 1, "Fatal exceptional case", extras 254 255 if not (pipe_read in input_fds): 256 # Read timed out, should be caught in the next loop 257 continue 258 259 output = os.read(pipe_read, 4096) 260 if not output: 261 # End of file from the child 262 break 263 264 out_stream.write(output) 265 266 os.close(pipe_read) 267 268 # wait for the child to complete 269 (somepid, exit_status) = os.waitpid(child_pid, 0) 270 process_end = time.time() 271 272 # Copy the output from the temporary file 273 out_stream.seek(0, 0) 274 extras['output'] = out_stream.read() 275 out_stream.close() 276 277 # Log script-output locally, unless we're asked not to 278 if log_output : 279 set_logfile(logfile_name) 280 log_to_file(0, extras['output']) 281 282 # since output can contain chars that won't make xmlrpc very happy, 283 # base64 encode it... 284 extras['base64enc'] = 1 285 extras['output'] = base64.encodestring(extras['output']) 286 287 extras['return_code'] = exit_status 288 289 # calculate start and end times in db's timespace 290 extras['process_start'] = db_now + (process_start - now) 291 extras['process_end'] = db_now + (process_end - now) 292 293 for key in ('process_start', 'process_end'): 294 extras[key] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(extras[key])) 295 296 # clean up the script 297 os.unlink(script_path) 298 299 if timed_out: 300 return 1, "Script killed, timeout of %s seconds exceeded" % timeout, extras 301 302 if exit_status == 0: 303 return 0, "Script executed", extras 304 305 return 1, "Script failed", extras
306