Package config_common :: Module transactions
[hide private]
[frames] | no frames]

Source Code for Module config_common.transactions

  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 shutil 
 18  import pwd 
 19  import grp 
 20  import sys 
 21  import errno 
 22  import shutil 
 23   
 24  from config_common import file_utils, utils, cfg_exceptions 
 25  from config_common.rhn_log import log_debug 
 26  from spacewalk.common.usix import raise_with_tb 
 27   
28 -class TargetNotFile(Exception): pass
29 -class DuplicateDeployment(Exception): pass
30 -class FailedRollback(Exception): pass
31 32 try: 33 from selinux import lsetfilecon 34 except:
35 - def lsetfilecon(temp_file_path, sectx):
36 # Do nothing RHEL 4 37 return 0
38 39 BACKUP_PREFIX = '/var/lib/rhncfg/backups' 40 BACKUP_EXTENSION = '.rhn-cfg-backup' 41
42 -class DeployTransaction:
43
44 - def __init__(self, transaction_root=None, auto_rollback=0):
45 # rollback transaction immediately upon failure? 46 self.auto_rollback = auto_rollback 47 # prepend all given paths 48 self.transaction_root = transaction_root 49 50 self.files = [] 51 self.dirs = [] 52 self.symlinks = [] 53 self.new_dirs = [] 54 self.backup_by_path = {} 55 self.newtemp_by_path = {} 56 self.changed_dir_info = {} 57 58 self.deployment_cb = None
59 60
61 - def _generate_backup_path(self, path):
62 return "%s%s%s" % (BACKUP_PREFIX, path, BACKUP_EXTENSION)
63 64
65 - def _rename_to_backup(self, path):
66 """renames a file to it's new backup name""" 67 # ensure we haven't attempted to back this file up before 68 # (protect against odd logic coming from the server) 69 if path in self.backup_by_path: 70 raise DuplicateDeployment("Error: attempted to backup %s twice" % path) 71 72 73 new_path = None 74 75 if os.path.exists(path): 76 # race 77 if os.path.isfile(path) or os.path.islink(path): 78 new_path = self._generate_backup_path(path) 79 log_debug(6, "renaming %s to backup %s ..." % (path, new_path)) 80 # os.renames will fail if the path and the new_path are on different partitions 81 # need to make sure to handle it if we catch a 'OSError: [Errno 18] Invalid cross-device link' 82 try: 83 log_debug(9, "trying to use os.renames") 84 oumask = os.umask(int('022', 8)) 85 os.renames(path, new_path) 86 os.umask(oumask) 87 except OSError: 88 e = sys.exc_info()[1] 89 if e.errno == 18: 90 log_debug(9, "os.renames failed, using shutil functions") 91 path_dir, path_file = os.path.split(path) 92 new_path_dir, new_path_file = os.path.split(new_path) 93 if os.path.isdir(new_path_dir): 94 if os.path.islink(path): 95 log_debug(9, "copying symlink %s to %s"% (path,new_path_dir)) 96 linkto = os.readlink(path) 97 if os.path.lexists(new_path): 98 log_debug(9, "backup %s exists, removing it"% (new_path)) 99 os.unlink(new_path) 100 os.symlink(linkto,new_path) 101 else: 102 log_debug(9, "backup directory %s exists, copying %s to it" % (new_path_dir, new_path_file)) 103 if os.path.lexists(new_path): 104 log_debug(9, "backup %s exists, removing it"% (new_path)) 105 os.unlink(new_path) 106 shutil.copy(path, new_path) 107 else: 108 log_debug(9, "backup directory does not exist, creating the tree now") 109 shutil.copytree(path_dir, new_path_dir, symlinks=0) 110 shutil.copy(path, new_path) 111 else: 112 raise 113 self.backup_by_path[path] = new_path 114 log_debug(9, "backed up to %s" % new_path) 115 else: 116 raise TargetNotFile("Error: %s is not a valid file, cannot create backup copy" % path) 117 return new_path
118 119
120 - def deploy_callback(self, cb):
121 self.deployment_cb = cb
122
123 - def _chown_chmod_chcon(self, temp_file_path, dest_path, file_info, strict_ownership=1):
124 if file_info['filetype'] != 'symlink': 125 uid = file_info.get('uid') 126 if uid is None: 127 if 'username' in file_info: 128 # determine uid 129 130 try: 131 user_record = pwd.getpwnam(file_info['username']) 132 uid = user_record[2] 133 except Exception: 134 e = sys.exc_info()[1] 135 #Check if username is an int 136 try: 137 uid = int(file_info['username']) 138 except ValueError: 139 raise_with_tb(cfg_exceptions.UserNotFound(file_info['username']), sys.exc_info()[2]) 140 else: 141 #default to root (3.2 sats) 142 uid = 0 143 144 gid = file_info.get('gid') 145 if gid is None: 146 if 'groupname' in file_info: 147 # determine gid 148 try: 149 group_record = grp.getgrnam(file_info['groupname']) 150 gid = group_record[2] 151 except Exception: 152 e = sys.exc_info()[1] 153 try: 154 gid = int(file_info['groupname']) 155 except ValueError: 156 raise_with_tb(cfg_exceptions.GroupNotFound(file_info['groupname']), sys.exc_info()[2]) 157 158 else: 159 #default to root (3.2 sats) 160 gid = 0 161 162 try: 163 if file_info['filetype'] != 'symlink': 164 os.chown(temp_file_path, uid, gid) 165 166 mode = '600' 167 if 'filemode' in file_info: 168 if file_info['filemode'] is "": 169 mode='000' 170 else: 171 mode = file_info['filemode'] 172 173 mode = int(str(mode), 8) 174 os.chmod(temp_file_path, mode) 175 176 if 'selinux_ctx' in file_info: 177 sectx = file_info.get('selinux_ctx') 178 if sectx is not None and sectx is not "": 179 log_debug(1, "selinux context: " + sectx); 180 try: 181 if lsetfilecon(temp_file_path, sectx) < 0: 182 raise Exception("failed to set selinux context on %s" % dest_path) 183 except OSError: 184 e = sys.exc_info()[1] 185 raise_with_tb(Exception("failed to set selinux context on %s" % dest_path, e), sys.exc_info()[2]) 186 187 except OSError: 188 e = sys.exc_info()[1] 189 if e.errno == errno.EPERM and not strict_ownership: 190 sys.stderr.write("cannonical file ownership and permissions lost on %s\n" % dest_path) 191 else: 192 raise
193 194 195
196 - def _normalize_path_to_root(self, path):
197 if self.transaction_root: 198 path = utils.normalize_path(self.transaction_root + os.sep + path) 199 return path
200
201 - def add_preprocessed(self, dest_path, processed_file_path, file_info, dirs_created, strict_ownership=1):
202 """preprocess the file if needed, and add the entry to the correct list""" 203 dest_path = self._normalize_path_to_root(dest_path) 204 log_debug(3, "preprocessing entry") 205 206 # If we get any dirs that were created by mkdir_p, add them here 207 if dirs_created: 208 self.new_dirs.extend(dirs_created) 209 210 # If the file is a directory, don't do all the file related work 211 # Older servers will not return directories; if filetype is missing, 212 # assume file 213 if file_info.get('filetype') == 'directory': 214 self.dirs.append(file_info) 215 else: 216 if "dest_path" in self.newtemp_by_path: 217 raise DuplicateDeployment("Error: %s already added to transaction" % dest_path) 218 self.newtemp_by_path[dest_path] = processed_file_path 219 self._chown_chmod_chcon(processed_file_path, dest_path, file_info, strict_ownership=strict_ownership)
220
221 - def add(self, file_info):
222 """add a file to the deploy transaction""" 223 for k in file_utils.FileProcessor.file_struct_fields: 224 if k not in file_info: 225 raise Exception("needed key %s mising from file structure" % k) 226 227 file_info['path'] = self._normalize_path_to_root(file_info['path']) 228 229 # Older servers will not return directories; if filetype is missing, 230 # assume file 231 if file_info.get('filetype') == 'directory': 232 self.dirs.append(file_info) 233 elif file_info.get('filetype') == 'symlink': 234 self.files.append(file_info) 235 else: 236 self.files.append(file_info)
237 238
239 - def rollback(self):
240 """revert the transaction""" 241 log_debug(3, "rolling back") 242 243 # restore old file from backup asap 244 for path in self.backup_by_path.keys(): 245 log_debug(6, "restoring %s from %s ..." % (path, self.backup_by_path[path])) 246 # os.rename will fail if the backup file and the old file are on different partitions 247 # need to make sure to handle it if we catch a 'OSError: [Errno 18] Invalid cross-device link' 248 try: 249 os.rename(self.backup_by_path[path], path) 250 except OSError: 251 e = sys.exc_info()[1] 252 if e.errno == 18: 253 log_debug(9, "os.rename failed, using shutil.copy") 254 shutil.copy(self.backup_by_path[path], path) 255 else: 256 raise 257 log_debug(9, "%s restored" % path) 258 259 # remove the temp files that we created 260 for tmp_file_path in self.newtemp_by_path.values(): 261 log_debug(6, "removing tmp file %s ..." % tmp_file_path) 262 os.unlink(tmp_file_path) 263 log_debug(9, "tmp file removed") 264 265 #revert the owner/perms of any directories that we changed 266 for d, val in self.changed_dir_info.items(): 267 log_debug(6, "reverting owner and perms of %s" % d) 268 self._chown_chmod_chcon(d, d, val) 269 log_debug(9, "directory reverted") 270 271 #remove any directories created by either mkdir_p or in the deploy 272 self.new_dirs.reverse() 273 for i in range(len(self.new_dirs)): 274 remove_dir = self.new_dirs[i] 275 log_debug(6, "removing directory %s that was created during transaction ..." % remove_dir) 276 if os.path.islink(remove_dir) == True: 277 os.remove(remove_dir) 278 else: 279 os.rmdir(remove_dir) 280 log_debug(9, "directory removed") 281 282 log_debug(3, "rollback successful")
283
284 - def deploy(self):
285 """attempt deployment; will rollback if auto_rollback is set""" 286 fp = file_utils.FileProcessor() 287 288 log_debug(3, "deploying transaction") 289 290 for dep_file in self.files: 291 if dep_file['filetype'] == 'symlink': 292 self.symlinks.append(dep_file) 293 294 # 0. handle any dirs we need to create first 295 # a) if the dir exists, then just change the mode and owners, 296 # else create it and then make sure the mode and owners are correct. 297 # b) if there are files, then continue 298 # 1. write new version (tmp) 299 # a) if anything breaks, remove all tmp versions and error out 300 # 2. rename old version to backup 301 # a) if anything breaks, rename all backed up files to original name, 302 # then do 1-a. 303 # 3. rename tmp to target name 304 # a) if anything breaks, remove all deployed files, then do 2-a. 305 # 306 # (yes, this leaves the backup version on disk...) 307 308 try: 309 310 # 0. 311 if self.dirs: 312 for directory in self.dirs: 313 dirname = self._normalize_path_to_root(directory['path']) 314 dirmode = directory['filemode'] 315 if os.path.isfile(dirname): 316 raise cfg_exceptions.DirectoryEntryIsFile(dirname) 317 if os.path.isdir(dirname): 318 s = os.stat(dirname) 319 entry = { 'filemode': "%o" % (s[0] & int('07777', 8)), 320 'uid': s[4], 321 'gid': s[5], 322 'filetype': 'directory', 323 } 324 self.changed_dir_info[dirname] = entry 325 log_debug(3, "directory found, chowning and chmoding to %s as needed: %s" % (dirmode, dirname)) 326 self._chown_chmod_chcon(dirname, dirname, directory) 327 else: 328 log_debug(3, "directory not found, creating: %s" % dirname) 329 dirs_created = utils.mkdir_p(dirname, None, self.symlinks, self.files) 330 self.new_dirs.extend(dirs_created) 331 self._chown_chmod_chcon(dirname, dirname, directory) 332 if self.deployment_cb: 333 self.deployment_cb(dirname) 334 335 log_debug(6, "changed_dir_info: %s" % self.changed_dir_info) 336 log_debug(4, "new_dirs: ", self.new_dirs) 337 338 339 if not self.newtemp_by_path and not self.files: 340 log_debug(4, "directory creation complete, no files found to create") 341 return 342 else: 343 log_debug(4, "done with directory creation, moving on to files") 344 345 # 1. 346 for dep_file in self.files: 347 path = dep_file['path'] 348 349 log_debug(6, "writing new version of %s to tmp file ..." % path) 350 # make any directories needed... 351 # 352 # TODO: it'd be nice if this had a hook for letting me know 353 # which ones are created... then i could clean created 354 # dirs on rollback 355 (directory, filename) = os.path.split(path) 356 if os.path.isdir(path) and not os.path.islink(path): 357 raise cfg_exceptions.FileEntryIsDirectory(path) 358 if not os.path.exists(directory):# and os.path.isdir(directory): 359 log_debug(7, "creating directories for %s ..." % directory) 360 dirs_created = utils.mkdir_p(directory, None, self.symlinks, self.files) 361 self.new_dirs.extend(dirs_created) 362 log_debug(7, "directories created and added to list for rollback") 363 364 # write the new contents to a tmp file, and store the path of the 365 # new tmp file by it's eventual target path 366 self.newtemp_by_path[path], temp_new_dirs = fp.process(dep_file, os.path.sep) 367 self.new_dirs.extend(temp_new_dirs or []) 368 369 # properly chown and chmod it 370 self._chown_chmod_chcon(self.newtemp_by_path[path], path, dep_file) 371 log_debug(9, "tempfile written: %s" % self.newtemp_by_path[path]) 372 373 374 #paths = map(lambda x: x['path'], self.files) 375 paths = list(self.newtemp_by_path.keys()) 376 377 # 2. 378 for path in paths: 379 if os.path.isdir(path) and not os.path.islink(path): 380 raise cfg_exceptions.FileEntryIsDirectory(path) 381 else: 382 self._rename_to_backup(path) 383 if path in self.backup_by_path: 384 log_debug(9, "backup file %s written" % self.backup_by_path[path]) 385 386 # 3. 387 paths.sort(key = lambda s: s.count(os.path.sep)) 388 for path in paths: 389 if self.deployment_cb: 390 self.deployment_cb(path) 391 log_debug(6, "deploying %s ..." % path) 392 os.rename(self.newtemp_by_path[path], path) 393 # race 394 del self.newtemp_by_path[path] 395 log_debug(9, "new version of %s deployed" % path) 396 397 log_debug(3, "deploy transaction successful") 398 399 except Exception: 400 #log_debug(1, traceback.format_exception_only(SyntaxError, e)) 401 #traceback.print_exc() 402 if self.auto_rollback: 403 self.rollback() 404 raise
405