Package entropy :: Package client :: Module misc

Source Code for Module entropy.client.misc

  1  # -*- coding: utf-8 -*- 
  2  """ 
  3   
  4      @author: Fabio Erculiani <[email protected]> 
  5      @contact: [email protected] 
  6      @copyright: Fabio Erculiani 
  7      @license: GPL-2 
  8   
  9      B{Entropy Package Manager Client Miscellaneous Interface}. 
 10   
 11  """ 
 12   
 13  import os 
 14  import sys 
 15  import shutil 
 16  import subprocess 
 17   
 18  from entropy.core.settings.base import SystemSettings 
 19  from entropy.const import etpConst, const_convert_to_rawstring, \ 
 20      const_convert_to_unicode, const_debug_write 
 21  from entropy.output import darkred, darkgreen, brown 
 22  from entropy.tools import getstatusoutput, rename_keep_permissions 
 23  from entropy.i18n import _ 
24 25 26 -def sharedinstlock(method):
27 """ 28 Decorator that acquires the Installed Packages Repository lock in 29 shared mode and calls the wrapped function with an extra argument 30 (the Installed Packages Repository object instance). 31 32 This decorator expects that "self" has an installed_repository() method 33 that returns the Installed Packages Repository instance. 34 """ 35 def wrapped(self, *args, **kwargs): 36 inst_repo = self.installed_repository() 37 with inst_repo.shared(): 38 return method(self, *args, **kwargs)
39 40 return wrapped 41
42 43 -def exclusiveinstlock(method):
44 """ 45 Decorator that acquires the Installed Packages Repository lock in 46 exclusive mode and calls the wrapped function with an extra 47 argument (the Installed Packages Repository object instance). 48 49 This decorator expects that "self" has an installed_repository() method 50 that returns the Installed Packages Repository instance. 51 """ 52 def wrapped(self, *args, **kwargs): 53 inst_repo = self.installed_repository() 54 with inst_repo.exclusive(): 55 return method(self, *args, **kwargs)
56 57 return wrapped 58
59 60 -class ConfigurationFiles(dict):
61 62 """ 63 Configuration Files Updates descriptor. 64 Each configuration file update action is described 65 as a mapping between the source file and its destination 66 target. 67 Each key (a string representing a source file) points to 68 a dictionary, containing the following items: 69 "destination": path to destination file (string) 70 "automerge": if source can be automerged to destination (bool) 71 72 This API is process and thread safe with regards to the Installed 73 Packages Repository. There is no need to do external locking on it. 74 """ 75
76 - def __init__(self, entropy_client, quiet=False):
77 self._quiet = quiet 78 self._entropy = entropy_client 79 self._settings = SystemSettings() 80 dict.__init__(self) 81 self._load()
82 83 @property
84 - def _repository_ids(self):
85 """ 86 Return a the list of repository identifiers the object 87 is using. 88 """ 89 inst_repo = self._entropy.installed_repository() 90 return [inst_repo.repository_id()]
91 92 @staticmethod
93 - def root():
94 """ 95 Return the current ROOT ("/") prefix 96 """ 97 return etpConst['systemroot']
98
99 - def _get_config_protect(self, mask=False):
100 """ 101 Get CONFIG_PROTECT or CONFIG_PROTECT_MASK values from 102 _repository_ids. 103 """ 104 misc_data = self._entropy.ClientSettings()['misc'] 105 config_protect = set() 106 # also ask to Source Package Manager 107 spm = self._entropy.Spm() 108 109 if mask: 110 config_protect |= misc_data['configprotectmask'] 111 # also read info from environment and merge here 112 config_protect |= set(spm.get_merge_protected_paths_mask()) 113 else: 114 config_protect |= misc_data['configprotect'] 115 # also read info from environment and merge here 116 config_protect |= set(spm.get_merge_protected_paths()) 117 118 # get from our repositories 119 for repository_id in self._repository_ids: 120 # assume that all the repositories need separate locking. 121 # this might be true in future. 122 repo = self._entropy.open_repository(repository_id) 123 with repo.shared(): 124 if mask: 125 _mask = repo.listConfigProtectEntries(mask = True) 126 else: 127 _mask = repo.listConfigProtectEntries() 128 129 config_protect |= set(_mask) 130 131 root = ConfigurationFiles.root() 132 config_protect = [root + x for x in config_protect] 133 config_protect.sort() 134 return config_protect
135
136 - def _encode_path(self, path):
137 """ 138 Encode path using proper encoding for use with os functions. 139 """ 140 try: 141 path = const_convert_to_rawstring( 142 path, from_enctype=etpConst['conf_encoding']) 143 except (UnicodeEncodeError,): 144 path = const_convert_to_rawstring( 145 path, from_enctype=sys.getfilesystemencoding()) 146 return path
147
148 - def _unicode_path(self, path):
149 """ 150 Convert a potentially raw string into a well formed unicode 151 one. Usually, this method is called on string that went through 152 _encode_path() 153 """ 154 try: 155 path = const_convert_to_unicode( 156 path, enctype=etpConst['conf_encoding']) 157 except (UnicodeDecodeError,): 158 path = const_convert_to_unicode( 159 path, enctype=sys.getfilesystemencoding()) 160 return path
161
162 - def _strip_root(self, path):
163 """ 164 Strip root prefix from path 165 """ 166 root = ConfigurationFiles.root() 167 new_path = path[len(root):] 168 return os.path.normpath(new_path)
169
170 - def _load_can_automerge(self, source, destination):
171 """ 172 Determine if source file path equals destination file path, 173 thus it can be automerged. 174 """ 175 def _vanished(): 176 # file went away? not really needed, but... 177 if not os.path.lexists(source): 178 return True 179 # broken symlink 180 if os.path.islink(source) and not os.path.exists(source): 181 return True 182 return False
183 184 if _vanished(): 185 return True 186 187 # first diff test 188 try: 189 exit_st = getstatusoutput( 190 'diff -Nua "%s" "%s" | grep ' 191 '"^[+-][^+-]" | grep -v \'# .Header:.*\'' % ( 192 source, destination,))[1] 193 except (OSError, IOError): 194 exit_st = 1 195 if exit_st == os.EX_OK: 196 return True 197 elif _vanished(): 198 return True 199 200 # second diff test 201 try: 202 exit_st = subprocess.call( 203 'diff -Bbua "%s" "%s" | ' 204 'egrep \'^[+-]\' | ' 205 'egrep -v \'^[+-][\t ]*#|^--- |^\+\+\+ \' | ' 206 'egrep -qv \'^[-+][\t ]*$\'' % ( 207 source, destination,), shell = True) 208 except (IOError, OSError,): 209 exit_st = 0 210 if exit_st == 1: 211 return True 212 213 if _vanished(): 214 return True 215 # requires manual merge 216 return False
217
218 - def _load_maybe_add(self, currentdir, item, filepath, number):
219 """ 220 Scan given path and store config file update information 221 if needed. 222 """ 223 try: 224 tofile = item[10:] 225 number = item[5:9] 226 except IndexError as err: 227 const_debug_write( 228 __name__, "load_maybe_add, IndexError: " 229 "%s, locals: %s" % ( 230 repr(err), locals())) 231 return 232 233 try: 234 int(number) 235 except ValueError as err: 236 # not a number 237 const_debug_write( 238 __name__, "load_maybe_add, ValueError: " 239 "%s, locals: %s" % ( 240 repr(err), locals())) 241 return 242 243 tofilepath = os.path.join(currentdir, tofile) 244 # tofile is the target filename now 245 # before adding, determine if we should automerge it 246 if self._load_can_automerge(filepath, tofilepath): 247 if not self._quiet: 248 self._entropy.output( 249 darkred("%s: %s") % ( 250 _("Automerging file"), 251 darkgreen(filepath), 252 ), 253 importance = 0, 254 level = "info" 255 ) 256 try: 257 rename_keep_permissions( 258 filepath, tofilepath) 259 except OSError as err: 260 const_debug_write( 261 __name__, "load_maybe_add, OSError: " 262 "%s, locals: %s" % ( 263 repr(err), locals())) 264 except IOError as err: 265 const_debug_write( 266 __name__, "load_maybe_add, IOError: " 267 "%s, locals: %s" % ( 268 repr(err), locals())) 269 return 270 271 # store 272 save_filepath = self._strip_root( 273 self._unicode_path(filepath)) 274 obj = { 275 'destination': self._strip_root( 276 self._unicode_path(tofilepath)), 277 'automerge': False, # redundant but backward compat 278 } 279 self[save_filepath] = obj 280 281 if not self._quiet: 282 self._entropy.output( 283 "%s: %s" % ( 284 brown(_("Found update")), 285 self._unicode_path( 286 darkgreen(filepath)),), 287 importance = 0, 288 level = "info" 289 )
290
291 - def _load(self):
292 """ 293 Load configuration file updates reading from disk. 294 """ 295 name_cache = set() 296 client_conf_protect = self._get_config_protect() 297 # NOTE: with Python 3.x we can remove const_convert... 298 # and avoid using _encode_path. 299 cfg_pfx = const_convert_to_rawstring("._cfg") 300 301 for path in client_conf_protect: 302 path = self._encode_path(path) 303 304 # is it a file? 305 scanfile = False 306 if os.path.isfile(path): 307 # find inside basename 308 path = os.path.dirname(path) 309 scanfile = True 310 311 for currentdir, _subdirs, files in os.walk(path): 312 for item in files: 313 if scanfile: 314 if path != item: 315 continue 316 317 if not item.startswith(cfg_pfx): 318 continue 319 320 # further check then 321 number = item[5:9] 322 try: 323 int(number) 324 except ValueError: 325 continue # not a valid etc-update file 326 if item[9] != "_": # no valid format provided 327 continue 328 329 filepath = os.path.join(currentdir, item) 330 if filepath in name_cache: 331 continue # skip, already done 332 name_cache.add(filepath) 333 334 self._load_maybe_add( 335 currentdir, item, filepath, number)
336
337 - def _backup(self, dest_path):
338 """ 339 Execute a backup of the given path if User enabled 340 the feature through Entropy Client configuration. 341 """ 342 client_settings = self._entropy.ClientSettings() 343 files_backup = client_settings['misc']['filesbackup'] 344 if not files_backup: 345 return 346 347 dest_path = self._encode_path(dest_path) 348 if not os.path.isfile(dest_path): 349 return 350 351 backup_pfx = self._encode_path("._entropy_backup.") 352 sep = self._encode_path("_") 353 dirname, basename = os.path.split(dest_path) 354 bcount = 0 355 356 bcount_str = self._encode_path("%d" % (bcount,)) 357 backup_path = os.path.join( 358 dirname, backup_pfx + bcount_str + sep + basename) 359 while os.path.lexists(backup_path): 360 bcount += 1 361 bcount_str = self._encode_path("%d" % (bcount,)) 362 backup_path = os.path.join( 363 dirname, backup_pfx + bcount_str + sep + basename) 364 365 # I don't know if copy2 likes bytes() 366 # time will tell! 367 try: 368 shutil.copy2(dest_path, backup_path) 369 except OSError as err: 370 const_debug_write( 371 __name__, "_backup, OSError: " 372 "%s, locals: %s" % ( 373 repr(err), locals())) 374 except IOError as err: 375 const_debug_write( 376 __name__, "_backup, IOError: " 377 "%s, locals: %s" % ( 378 repr(err), locals()))
379
380 - def remove(self, source):
381 """ 382 Remove proposed source configuration file. 383 "source" must be a key of this dictionary, if 384 not, True is returned. If file pointed at source 385 doesn't exist or removal fails, False is returned. 386 """ 387 obj = self.pop(source, None) 388 if obj is None: 389 return True 390 391 root = ConfigurationFiles.root() 392 source_file = root + source 393 source_file = self._encode_path(source_file) 394 try: 395 os.remove(source_file) 396 except OSError as err: 397 const_debug_write( 398 __name__, "remove, OSError: " 399 "%s, locals: %s" % ( 400 repr(err), locals())) 401 return False 402 return True
403
404 - def merge(self, source):
405 """ 406 Merge proposed source configuration file. 407 "source" must be a key of this dictionary, if 408 not, True is returned. If file pointed at source 409 doesn't exist or merge fails, False is returned. 410 """ 411 obj = self.pop(source, None) 412 if obj is None: 413 return True 414 415 root = ConfigurationFiles.root() 416 source_file = root + source 417 dest_file = root + obj['destination'] 418 self._backup(dest_file) 419 source_file = self._encode_path(source_file) 420 dest_file = self._encode_path(dest_file) 421 try: 422 rename_keep_permissions( 423 source_file, dest_file) 424 except OSError as err: 425 const_debug_write( 426 __name__, "merge, OSError: " 427 "%s, locals: %s" % ( 428 repr(err), locals())) 429 return False 430 return True
431
432 - def exists(self, path):
433 """ 434 Return True if path exists. 435 This methods automatically appends the ROOT 436 prefix and handles unicode correctly 437 """ 438 root = ConfigurationFiles.root() 439 source_file = root + path 440 source_file = self._encode_path(source_file) 441 return os.path.lexists(source_file)
442
443 444 -class ConfigurationUpdates:
445 446 """ 447 Entropy Configuration File Updates management class. 448 """ 449
450 - def __init__(self, entropy_client, _config_class=None):
451 if _config_class is None: 452 self._config_class = ConfigurationFiles 453 else: 454 self._config_class = _config_class 455 self._entropy = entropy_client 456 self._settings = self._entropy.Settings()
457
458 - def get(self, quiet=False):
459 """ 460 Return a new ConfigurationFiles object. 461 """ 462 return self._config_class(self._entropy)
463