"Fossies" - the Fresh Open Source Software Archive

Member "backintime-1.2.0/common/mount.py" (27 Apr 2019, 37528 Bytes) of package /linux/privat/backintime-1.2.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "mount.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.24_vs_1.2.0.

    1 #    Copyright (C) 2012-2019 Germar Reitze, Taylor Raack
    2 #
    3 #    This program is free software; you can redistribute it and/or modify
    4 #    it under the terms of the GNU General Public License as published by
    5 #    the Free Software Foundation; either version 2 of the License, or
    6 #    (at your option) any later version.
    7 #
    8 #    This program is distributed in the hope that it will be useful,
    9 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
   10 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   11 #    GNU General Public License for more details.
   12 #
   13 #    You should have received a copy of the GNU General Public License along
   14 #    with this program; if not, write to the Free Software Foundation, Inc.,
   15 #    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   16 
   17 import os
   18 import subprocess
   19 import json
   20 import gettext
   21 from zlib import crc32
   22 from time import sleep
   23 
   24 import config
   25 import logger
   26 import tools
   27 import password
   28 from exceptions import MountException, HashCollision
   29 
   30 _=gettext.gettext
   31 
   32 class Mount(object):
   33     """
   34     This is the high-level mount API. This will handle mount, umount, remount
   35     and checks on the low-level :py:class:`MountControl` subclass backends for
   36     BackInTime.
   37 
   38     If ``cfg`` is ``None`` this will load the default config. If ``profile_id``
   39     is ``None`` it will use
   40     :py:func:`configfile.ConfigFileWithProfiles.currentProfile`.
   41 
   42     If the current profile uses Password-Cache and the Password-Cache is not
   43     running this will try to start it.
   44 
   45     Args:
   46         cfg (config.Config):    current config
   47         profile_id (str):       profile ID that should be used
   48         tmp_mount (bool):       if ``True`` mount to a temporary destination
   49         parent (QWidget):       parent widget for QDialogs or ``None`` if there
   50                                 is no parent
   51     """
   52     def __init__(self,
   53                  cfg = None,
   54                  profile_id = None,
   55                  tmp_mount = False,
   56                  parent = None):
   57         self.config = cfg
   58         if self.config is None:
   59             self.config = config.Config()
   60 
   61         self.profile_id = profile_id
   62         if self.profile_id is None:
   63             self.profile_id = self.config.currentProfile()
   64 
   65         self.tmp_mount = tmp_mount
   66         self.parent = parent
   67 
   68         if self.config.passwordUseCache(self.profile_id):
   69             cache = password.Password_Cache(self.config)
   70             action = None
   71             running = cache.status()
   72             if not running:
   73                 logger.debug('pw-cache is not running', self)
   74                 action = 'start'
   75             if running and not cache.checkVersion():
   76                 logger.debug('pw-cache is running but is an old version', self)
   77                 action = 'restart'
   78             bit = tools.which('backintime')
   79             if not action is None and not bit is None and len(bit):
   80                 cmd = [bit, 'pw-cache', action]
   81                 logger.debug('Call command: %s'
   82                              %' '.join(cmd), self)
   83                 proc = subprocess.Popen(cmd,
   84                                         stdout = subprocess.DEVNULL,
   85                                         stderr = subprocess.DEVNULL)
   86                 if proc.returncode:
   87                     logger.error('Failed to %s pw-cache: %s'
   88                                  %(action, proc.returncode),
   89                                  self)
   90                     pass
   91 
   92     def mount(self, mode = None, check = True, **kwargs):
   93         """
   94         High-level `mount`. Check if the selected ``mode`` need to be mounted,
   95         select the low-level backend and mount it.
   96 
   97         Args:
   98             mode (str):     mode to use. One of 'local', 'ssh', 'local_encfs' or
   99                             'ssh_encfs'
  100             check (bool):   if ``True`` run
  101                             :py:func:`MountControl.preMountCheck` before
  102                             mounting
  103             **kwargs:       keyword arguments paste to low-level
  104                             :py:class:`MountControl` subclass backend
  105 
  106         Returns:
  107             str:            Hash ID used as mountpoint
  108 
  109         Raises:
  110             exceptions.MountException:
  111                             if a check failed
  112             exceptions.HashCollision:
  113                             if Hash ID was used before but umount info wasn't
  114                             identical
  115         """
  116         self.config.PLUGIN_MANAGER.load(cfg = self.config)
  117         self.config.PLUGIN_MANAGER.mount(self.profile_id)
  118         if mode is None:
  119             mode = self.config.snapshotsMode(self.profile_id)
  120 
  121         if self.config.SNAPSHOT_MODES[mode][0] is None:
  122             #mode doesn't need to mount
  123             return 'local'
  124         else:
  125             while True:
  126                 try:
  127                     mounttools = self.config.SNAPSHOT_MODES[mode][0]
  128                     backend = mounttools(cfg = self.config,
  129                                          profile_id = self.profile_id,
  130                                          tmp_mount = self.tmp_mount,
  131                                          mode = mode,
  132                                          parent = self.parent,
  133                                          **kwargs)
  134                     return backend.mount(check = check)
  135                 except HashCollision as ex:
  136                     logger.warning(str(ex), self)
  137                     del backend
  138                     check = False
  139                     continue
  140                 break
  141 
  142     def umount(self, hash_id = None):
  143         """
  144         High-level `unmount`. Unmount the low-level backend. This will read
  145         unmount infos written next to the mountpoint identified by ``hash_id``
  146         and unmount it.
  147 
  148         Args:
  149             hash_id (bool): Hash ID used as mountpoint before that should get
  150                             unmounted
  151 
  152         Raises:
  153             exceptions.MountException:
  154                             if a check failed
  155         """
  156         self.config.PLUGIN_MANAGER.load(cfg = self.config)
  157         self.config.PLUGIN_MANAGER.unmount(self.profile_id)
  158         if hash_id is None:
  159             hash_id = self.config.current_hash_id
  160         if hash_id == 'local':
  161             #mode doesn't need to umount
  162             return
  163         else:
  164             umount_info = os.path.join(self.config._LOCAL_MOUNT_ROOT, hash_id, 'umount')
  165             with open(umount_info, 'r') as f:
  166                 data_string = f.read()
  167                 f.close()
  168             kwargs = json.loads(data_string)
  169             mode = kwargs.pop('mode')
  170             mounttools = self.config.SNAPSHOT_MODES[mode][0]
  171             backend = mounttools(cfg = self.config,
  172                                  profile_id = self.profile_id,
  173                                  tmp_mount = self.tmp_mount,
  174                                  mode = mode,
  175                                  hash_id = hash_id,
  176                                  parent = self.parent,
  177                                  **kwargs)
  178             backend.umount()
  179 
  180     def preMountCheck(self, mode = None, first_run = False, **kwargs):
  181         """
  182         High-level check. Run :py:func:`MountControl.preMountCheck` to check
  183         if all conditions for :py:func:`Mount.mount` are set.
  184 
  185         Should be called with ``first_run = True`` to check if new settings are
  186         correct before saving them.
  187 
  188         Args:
  189             mode (str):         mode to use. One of 'local', 'ssh',
  190                                 'local_encfs' or 'ssh_encfs'
  191             first_run (bool):   run intense checks that only need to run after
  192                                 changing settings but not every time before
  193                                 mounting
  194             **kwargs:           keyword arguments paste to low-level
  195                                 :py:class:`MountControl` subclass backend
  196 
  197         Returns:
  198             bool:               ``True`` if all checks where okay
  199 
  200         Raises:
  201             exceptions.MountException:
  202                                 if a check failed
  203         """
  204         if mode is None:
  205             mode = self.config.snapshotsMode(self.profile_id)
  206 
  207         if self.config.SNAPSHOT_MODES[mode][0] is None:
  208             #mode doesn't need to mount
  209             return True
  210         else:
  211             mounttools = self.config.SNAPSHOT_MODES[mode][0]
  212             backend = mounttools(cfg = self.config,
  213                                  profile_id = self.profile_id,
  214                                  tmp_mount = self.tmp_mount,
  215                                  mode = mode,
  216                                  parent = self.parent,
  217                                  **kwargs)
  218             return backend.preMountCheck(first_run)
  219 
  220     def remount(self, new_profile_id, mode = None, hash_id = None, **kwargs):
  221         """
  222         High-level `remount`. Unmount the old profile presented by ``hash_id``
  223         and mount new profile ``new_profile_id`` with mode ``mode``. If old and
  224         new mounts are the same just add new symlinks and keep the mount.
  225 
  226         Args map to profiles::
  227 
  228             new_profile_id          <= new profile
  229             mode                    <= new profile
  230             kwargs                  <= new profile
  231             hash_id                 <= old profile
  232             self.profile_id         <= old profile
  233 
  234         Args:
  235             new_profile_id (str):   Profile ID that should get mounted
  236             mode (str):             mode to use for new mount. One of 'local',
  237                                     'ssh', 'local_encfs' or 'ssh_encfs'
  238             hash_id (str):          Hash ID used as mountpoint on the old mount,
  239                                     that should get unmounted
  240             **kwargs:               keyword arguments paste to low-level
  241                                     :py:class:`MountControl` subclass backend
  242                                     for the new mount
  243 
  244         Returns:
  245             str:                    Hash ID used as mountpoint
  246 
  247         Raises:
  248             exceptions.MountException:
  249                                     if a check failed
  250             exceptions.HashCollision:
  251                                     if Hash ID was used before but umount info
  252                                     wasn't identical
  253         """
  254         if mode is None:
  255             mode = self.config.snapshotsMode(new_profile_id)
  256         if hash_id is None:
  257             hash_id = self.config.current_hash_id
  258 
  259         if self.config.SNAPSHOT_MODES[mode][0] is None:
  260             #new profile don't need to mount.
  261             self.umount(hash_id = hash_id)
  262             return 'local'
  263 
  264         if hash_id == 'local':
  265             #old profile don't need to umount.
  266             self.profile_id = new_profile_id
  267             return self.mount(mode = mode, **kwargs)
  268 
  269         mounttools = self.config.SNAPSHOT_MODES[mode][0]
  270         backend = mounttools(cfg = self.config,
  271                              profile_id = new_profile_id,
  272                              tmp_mount = self.tmp_mount,
  273                              mode = mode,
  274                              parent = self.parent,
  275                              **kwargs)
  276         if backend.compareRemount(hash_id):
  277             #profiles uses the same settings. just swap the symlinks
  278             backend.removeSymlink(profile_id = self.profile_id)
  279             backend.setSymlink(profile_id = new_profile_id, hash_id = hash_id)
  280             return hash_id
  281         else:
  282             #profiles are different. we need to umount and mount again
  283             self.umount(hash_id = hash_id)
  284             self.profile_id = new_profile_id
  285             return self.mount(mode = mode, **kwargs)
  286 
  287 class MountControl(object):
  288     """
  289     This is the low-level mount API. This should be subclassed by backends.
  290 
  291     Subclasses should have its own ``__init__`` but **must** also call the
  292     inherited ``__init__``.
  293 
  294     You **must** overwrite methods:\n
  295         :py:func:`MountControl._mount`
  296 
  297     You **can** overwrite methods:\n
  298         :py:func:`MountControl._umount`\n
  299         :py:func:`MountControl.preMountCheck`\n
  300         :py:func:`MountControl.postMountCheck`\n
  301         :py:func:`MountControl.preUmountCheck`\n
  302         :py:func:`MountControl.postUmountCheck`
  303 
  304     These arguments **must** be defined in ``self`` namespace by
  305     subclassing ``__init__`` method:\n
  306         mountproc (str):        process used to mount\n
  307         log_command (str):      shortened form of mount command used in logs\n
  308         symlink_subfolder (str):mountpoint-subfolder which should be linked\n
  309 
  310     Args:
  311         cfg (config.Config):    current config
  312         profile_id (str):       profile ID that should be used
  313         hash_id (str):          crc32 hash used to identify identical mountpoints
  314         tmp_mount (bool):       if ``True`` mount to a temporary destination
  315         parent (QWidget):       parent widget for QDialogs or ``None`` if there
  316                                 is no parent
  317         symlink (bool):         if ``True`` set symlink to mountpoint
  318         mode (str):             one of ``local``, ``local_encfs``, ``ssh`` or
  319                                 ``ssh_encfs``
  320         hash_collision (int):   global value used to prevent hash collisions on
  321                                 mountpoints
  322     """
  323 
  324     CHECK_FUSE_GROUP = False
  325 
  326     def __init__(self,
  327                  cfg = None,
  328                  profile_id = None,
  329                  hash_id = None,
  330                  tmp_mount = False,
  331                  parent = None,
  332                  symlink = True,
  333                  *args,
  334                  **kwargs):
  335         self.config = cfg
  336         if self.config is None:
  337             self.config = config.Config()
  338 
  339         self.profile_id = profile_id
  340         if self.profile_id is None:
  341             self.profile_id = self.config.currentProfile()
  342 
  343         self.tmp_mount = tmp_mount
  344         self.hash_id = hash_id
  345         self.parent = parent
  346         self.symlink = symlink
  347 
  348         self.local_host = self.config.host()
  349         self.local_user = self.config.user()
  350         self.pid = self.config.pid()
  351 
  352         self.all_kwargs = {}
  353 
  354         self.setattrKwargs('mode', self.config.snapshotsMode(self.profile_id), **kwargs)
  355         self.setattrKwargs('hash_collision', self.config.hashCollision(), **kwargs)
  356 
  357     def setDefaultArgs(self):
  358         """
  359         Set some arguments which are necessary for all backends.
  360         ``self.all_kwargs`` need to be filled through :py:func:`setattrKwargs`
  361         before calling this.
  362         """
  363         #self.destination should contain all arguments that are nessesary for
  364         #mount.
  365         args = list(self.all_kwargs.keys())
  366         self.destination = '%s:' % self.all_kwargs['mode']
  367         args.remove('mode')
  368         args.sort()
  369         for arg in args:
  370             self.destination += ' %s' % self.all_kwargs[arg]
  371 
  372         #unique id for every different mount settings. Similar settings even in
  373         #different profiles will generate the same hash_id and so share the same
  374         #mountpoint
  375         if self.hash_id is None:
  376             self.hash_id = self.hash(self.destination)
  377 
  378         self.mount_root = self.config._LOCAL_MOUNT_ROOT
  379         self.snapshots_path = self.config.snapshotsPath(profile_id = self.profile_id,
  380                                                              mode = self.mode,
  381                                                              tmp_mount = self.tmp_mount)
  382 
  383         self.hash_id_path = self.hashIdPath()
  384         self.currentMountpoint = self.mountpoint()
  385         self.lock_path = self.lockPath()
  386         self.umount_info = self.umountInfoPath()
  387 
  388     def mount(self, check = True):
  389         """
  390         Low-level `mount`. Set mountprocess lock and prepair mount, run checks
  391         and than call :py:func:`_mount` for the subclassed backend. Finally set
  392         mount lock and symlink and release mountprocess lock.
  393 
  394         Args:
  395             check (bool):   if ``True`` run :py:func:`preMountCheck` before
  396                             mounting
  397 
  398         Returns:
  399             str:            Hash ID used as mountpoint
  400 
  401         Raises:
  402             exceptions.MountException:
  403                             if a check failed
  404             exceptions.HashCollision:
  405                             if Hash ID was used before but umount info wasn't
  406                             identical
  407         """
  408         self.createMountStructure()
  409         self.mountProcessLockAcquire()
  410         try:
  411             if self.mounted():
  412                 if not self.compareUmountInfo():
  413                     #We probably have a hash collision
  414                     self.config.incrementHashCollision()
  415                     raise HashCollision(_('Hash collision occurred in hash_id %s. Incrementing global value hash_collision and try again.') % self.hash_id)
  416                 logger.info('Mountpoint %s is already mounted' %self.currentMountpoint, self)
  417             else:
  418                 if check:
  419                     self.preMountCheck()
  420                 self._mount()
  421                 self.postMountCheck()
  422                 logger.info('mount %s on %s'
  423                             %(self.log_command, self.currentMountpoint),
  424                             self)
  425                 self.writeUmountInfo()
  426         except Exception:
  427             raise
  428         else:
  429             self.mountLockAquire()
  430             self.setSymlink()
  431         finally:
  432             self.mountProcessLockRelease()
  433         return self.hash_id
  434 
  435     def umount(self):
  436         """
  437         Low-level `umount`. Set mountprocess lock, run umount checks and call
  438         :py:func:`_umount` for the subclassed backend. Finally release
  439         mount lock, remove symlink and release mountprocess lock.
  440 
  441         Raises:
  442             exceptions.MountException:  if a check failed
  443         """
  444         self.mountProcessLockAcquire()
  445         try:
  446             if not os.path.isdir(self.hash_id_path):
  447                 logger.info('Mountpoint %s does not exist.' % self.currentMountpoint, self)
  448             else:
  449                 if not self.mounted():
  450                     logger.info('Mountpoint %s is not mounted' % self.currentMountpoint, self)
  451                 else:
  452                     if self.mountLockCheck():
  453                         logger.info('Mountpoint %s still in use. Keep mounted' % self.currentMountpoint, self)
  454                     else:
  455                         self.preUmountCheck()
  456                         self._umount()
  457                         self.postUmountCheck()
  458                         if os.listdir(self.currentMountpoint):
  459                             logger.warning('Mountpoint %s not empty after unmount' %self.currentMountpoint, self)
  460                         else:
  461                             logger.info('unmount %s from %s'
  462                                         %(self.log_command, self.currentMountpoint),
  463                                         self)
  464         except Exception:
  465             raise
  466         else:
  467             self.mountLockRelease()
  468             self.removeSymlink()
  469         finally:
  470             self.mountProcessLockRelease()
  471 
  472     def _mount(self):
  473         """
  474         Backend mount method. This **must** be overwritten in the backend which
  475         subclasses :py:class:`MountControl`.
  476         """
  477         raise NotImplementedError('_mount need to be overwritten in backend')
  478 
  479     def _umount(self):
  480         """
  481         Unmount with ``fusermount -u`` for fuse based backends. This **can** be
  482         overwritten by backends which subclasses :py:class:`MountControl`.
  483 
  484         Raises:
  485             exceptions.MountException:  if unmount failed
  486         """
  487         try:
  488             subprocess.check_call(['fusermount', '-u', self.currentMountpoint])
  489         except subprocess.CalledProcessError:
  490             raise MountException(_('Can\'t unmount %(proc)s from %(mountpoint)s')
  491                                   %{'proc': self.mountproc,
  492                                     'mountpoint': self.currentMountpoint})
  493 
  494     def preMountCheck(self, first_run = False):
  495         """
  496         Check what ever conditions must be given for the mount to be done
  497         successful. This **can** be overwritten in backends which
  498         subclasses :py:class:`MountControl`.
  499 
  500         Returns:
  501             bool:       ``True`` if all checks where okay
  502 
  503         Raises:
  504             exceptions.MountException:
  505                         if backend can not mount
  506 
  507         Note:
  508             This can also be used to prepare things before running
  509             :py:func:`_mount`
  510         """
  511         return True
  512 
  513     def postMountCheck(self):
  514         """
  515         Check if the mount was successful. This **can** be overwritten in
  516         backends which subclasses :py:class:`MountControl`.
  517 
  518         Returns:
  519             bool:       ``True`` if all checks where okay
  520 
  521         Raises:
  522             exceptions.MountException:
  523                         if backend wasn't mount successful
  524 
  525         Note:
  526             This can also be used to clean up after running :py:func:`_mount`
  527         """
  528         return True
  529 
  530     def preUmountCheck(self):
  531         """
  532         Check if backend is safe to umount. This **can** be overwritten in
  533         backends which subclasses :py:class:`MountControl`.
  534 
  535         Returns:
  536             bool:       ``True`` if all checks where okay
  537 
  538         Raises:
  539             exceptions.MountException:
  540                         if backend can not umount
  541 
  542         Note:
  543             This can also be used to prepare things before running
  544             :py:func:`_umount`
  545         """
  546         return True
  547 
  548     def postUmountCheck(self):
  549         """
  550         Check if unmount was successful. This **can** be overwritten in backends
  551         which subclasses :py:class:`MountControl`.
  552 
  553         Returns:
  554             bool:       ``True`` if all checks where okay
  555 
  556         Raises:
  557             exceptions.MountException:
  558                         if backend wasn't unmounted successful
  559 
  560         Note:
  561             This can also be used to clean up after running :py:func:`_umount`
  562         """
  563         return True
  564 
  565     def checkFuse(self):
  566         """
  567         Check if command in self.mountproc is installed and user is part of
  568         group ``fuse``.
  569 
  570         Raises:
  571             exceptions.MountException:  if either command is not available or
  572                                         user is not in group fuse
  573         """
  574         logger.debug('Check fuse', self)
  575         if not tools.checkCommand(self.mountproc):
  576             logger.debug('%s is missing' %self.mountproc, self)
  577             raise MountException(_('%(proc)s not found. Please install e.g. %(install_command)s')
  578                                   %{'proc': self.mountproc,
  579                                     'install_command': "'apt-get install %s'" %self.mountproc})
  580         if self.CHECK_FUSE_GROUP:
  581             user = self.config.user()
  582             try:
  583                 fuse_grp_members = grp.getgrnam('fuse')[3]
  584             except KeyError:
  585                 #group fuse doesn't exist. So most likely it isn't used by this distribution
  586                 logger.debug("Group fuse doesn't exist. Skip test", self)
  587                 return
  588             if not user in fuse_grp_members:
  589                 logger.debug('User %s is not in group fuse' %user, self)
  590                 raise MountException(_('%(user)s is not member of group \'fuse\'.\n '
  591                                         'Run \'sudo adduser %(user)s fuse\'. To apply '
  592                                         'changes logout and login again.\nLook at '
  593                                         '\'man backintime\' for further instructions.')
  594                                         % {'user': user})
  595 
  596     def mounted(self):
  597         """
  598         Check if the mountpoint is already mounted.
  599 
  600         Returns:
  601             bool:   ``True`` if mountpoint is mounted
  602 
  603         Raises:
  604             exceptions.MountException:
  605                     if mountpoint is not mounted but also not empty
  606         """
  607         if os.path.ismount(self.currentMountpoint):
  608             return True
  609         else:
  610             if os.listdir(self.currentMountpoint):
  611                 raise MountException(_('mountpoint %s not empty.') % self.currentMountpoint)
  612             return False
  613 
  614     def createMountStructure(self):
  615         """
  616         Create folders that are necessary for mounting.
  617 
  618         Folder structure in ~/.local/share/backintime/mnt/ (self.mount_root)::
  619 
  620             |\ <pid>.lock              <=  mountprocess lock that will prevent
  621             |                              different processes modifying
  622             |                              mountpoints at one time
  623             |
  624             |\ <hash_id>/              <=  ``self.hash_id_path``
  625             |            \                 will be shared by all profiles with
  626             |            |                 the same mount settings
  627             |            |
  628             |            |\ mountpoint/<=  ``self.currentMountpoint``
  629             |            |                 real mountpoint
  630             |            |
  631             |            |\ umount     <=  ``self.umount_info``
  632             |            |                 json file with all nessesary args
  633             |            |                 for unmount
  634             |            |
  635             |            \  locks/     <=  ``self.lock_path``
  636             |                              for each process you have a
  637             |                              ``<pid>.lock`` file
  638             |
  639             |\ <profile id>_<pid>/     <=  sym-link to the right path. return by
  640             |                              config.snapshotsPath
  641             |                              (can be ../mnt/<hash_id>/mount_point
  642             |                              for ssh or
  643             |                              ../mnt/<hash_id>/<HOST>/<SHARE> for
  644             |                              fusesmb ...)
  645             |
  646             \ tmp_<profile id>_<pid>/ <=  sym-link for testing mountpoints in
  647                                           settingsdialog
  648         """
  649         tools.mkdir(self.mount_root, 0o700)
  650         tools.mkdir(self.hash_id_path, 0o700)
  651         tools.mkdir(self.currentMountpoint, 0o700)
  652         tools.mkdir(self.lock_path, 0o700)
  653 
  654     def mountProcessLockAcquire(self, timeout = 60):
  655         """
  656         Create a short term lock only for blocking other processes changing
  657         mounts at the same time.
  658 
  659         Args:
  660             timeout (int):  wait ``timeout`` seconds before fail acquiring
  661                             the lock
  662 
  663         Raises:
  664             exceptions.MountException:
  665                             if timed out
  666         """
  667         lock_path = self.mount_root
  668         lockSuffix = '.lock'
  669         lock = os.path.join(lock_path, self.pid + lockSuffix)
  670         count = 0
  671         while self.checkLocks(lock_path, lockSuffix):
  672             count += 1
  673             if count == timeout:
  674                 raise MountException(_('Mountprocess lock timeout'))
  675             sleep(1)
  676 
  677         logger.debug('Acquire mountprocess lock %s'
  678                      %lock, self)
  679         with open(lock, 'w') as f:
  680             f.write(self.pid)
  681 
  682     def mountProcessLockRelease(self):
  683         """
  684         Remove mountprocess lock.
  685         """
  686         lock_path = self.mount_root
  687         lockSuffix = '.lock'
  688         lock = os.path.join(lock_path, self.pid + lockSuffix)
  689         logger.debug('Release mountprocess lock %s'
  690                      %lock, self)
  691         if os.path.exists(lock):
  692             os.remove(lock)
  693 
  694     def mountLockAquire(self):
  695         """
  696         Create a lock for a mountpoint to prevent unmounting as long as this
  697         process is still running.
  698         """
  699         if self.tmp_mount:
  700             lockSuffix = '.tmp.lock'
  701         else:
  702             lockSuffix = '.lock'
  703         lock = os.path.join(self.lock_path, self.pid + lockSuffix)
  704         logger.debug('Set mount lock %s'
  705                      %lock, self)
  706         with open(lock, 'w') as f:
  707             f.write(self.pid)
  708 
  709     def mountLockCheck(self):
  710         """
  711         Check for locks on the current mountpoint.
  712 
  713         Returns:
  714             bool:   ``True`` if there are any locks
  715         """
  716         lockSuffix = '.lock'
  717         return self.checkLocks(self.lock_path, lockSuffix)
  718 
  719     def mountLockRelease(self):
  720         """
  721         Remove mountpoint lock for this process.
  722         """
  723         if self.tmp_mount:
  724             lockSuffix = '.tmp.lock'
  725         else:
  726             lockSuffix = '.lock'
  727         lock = os.path.join(self.lock_path, self.pid + lockSuffix)
  728         if os.path.exists(lock):
  729             logger.debug('Remove mount lock %s'
  730                          %lock, self)
  731             os.remove(lock)
  732 
  733     def checkLocks(self, path, lockSuffix):
  734         """
  735         Check if there are active locks ending with ``lockSuffix`` in ``path``.
  736         If the process owning the lock doesn't exist anymore this will remove
  737         the lock.
  738 
  739         Args:
  740             path (str):         full path to lock directory
  741             lockSuffix (str):   last part of locks name
  742 
  743         Returns:
  744             bool:               ``True`` if there are active locks in ``path``
  745         """
  746         for f in os.listdir(path):
  747             if not f[-len(lockSuffix):] == lockSuffix:
  748                 continue
  749             is_tmp = os.path.basename(f)[-len(lockSuffix)-len('.tmp'):-len(lockSuffix)] == '.tmp'
  750             if is_tmp:
  751                 lock_pid = os.path.basename(f)[:-len('.tmp')-len(lockSuffix)]
  752             else:
  753                 lock_pid = os.path.basename(f)[:-len(lockSuffix)]
  754             if lock_pid == self.pid:
  755                 if is_tmp == self.tmp_mount:
  756                     continue
  757             if tools.processAlive(int(lock_pid)):
  758                 return True
  759             else:
  760                 logger.debug('Remove old and invalid lock %s'
  761                              %f, self)
  762                 #clean up
  763                 os.remove(os.path.join(path, f))
  764                 for symlink in os.listdir(self.mount_root):
  765                     if symlink.endswith('_%s' % lock_pid):
  766                         os.remove(os.path.join(self.mount_root, symlink))
  767         return False
  768 
  769     def setattrKwargs(self, arg, default, store = True, **kwargs):
  770         """
  771         Set attribute ``arg`` in local namespace (self.arg). Also collect all
  772         args in ``self.all_kwargs`` which will be hashed later and used as
  773         mountpoint name and also be written as unmount_info.
  774 
  775         Args:
  776             arg (str):      argument name
  777             default:        default value used if ``arg`` is not in ``kwargs``
  778             store (bool):   if ``True`` add ``arg`` to ``self.all_kwargs``
  779             **kwargs:       arguments given on backend constructor
  780         """
  781         if arg in kwargs:
  782             value = kwargs[arg]
  783         else:
  784             value = default
  785         setattr(self, arg, value)
  786         if store:
  787             #make dictionary with all used args for umount
  788             self.all_kwargs[arg] = value
  789 
  790     def writeUmountInfo(self):
  791         """
  792         Write content of ``self.all_kwargs`` to file
  793         ``~/.local/share/backintime/mnt/<hash_id>/umount``.
  794         This will be used to unmount the filesystem later.
  795         """
  796         data_string = json.dumps(self.all_kwargs)
  797         with open(self.umount_info, 'w') as f:
  798             f.write(data_string)
  799             f.close
  800 
  801     def readUmountInfo(self, umount_info = None):
  802         """
  803         Read keyword arguments from file ``umount_info``.
  804 
  805         Args:
  806             umount_info (str):  full path to <hash_id>/umount file. If ``None``
  807                                 current ``<hash_id>/umount`` file will be used
  808 
  809         Returns:
  810             dict:               previously written ``self.all_kwargs``
  811         """
  812         if umount_info is None:
  813             umount_info = self.umount_info
  814         with open(umount_info, 'r') as f:
  815             data_string = f.read()
  816             f.close()
  817         return json.loads(data_string)
  818 
  819     def compareUmountInfo(self, umount_info = None):
  820         """
  821         Compare current ``self.all_kwargs`` with those from file ``umount_info``.
  822 
  823         This should prevent hash collisions of two different mounts.
  824 
  825         Args:
  826             umount_info (str):  full path to <hash_id>/umount file
  827 
  828         Returns:
  829             bool:               ``True`` if ``self.all_kwargs`` and ``kwargs``
  830                                 read from ``umount_info`` file are identiacal
  831         """
  832         #run self.all_kwargs through json first
  833         current_kwargs = json.loads(json.dumps(self.all_kwargs))
  834         saved_kwargs = self.readUmountInfo(umount_info)
  835         if not len(current_kwargs) == len(saved_kwargs):
  836             return False
  837         for arg in list(current_kwargs.keys()):
  838             if not arg in list(saved_kwargs.keys()):
  839                 return False
  840             if not current_kwargs[arg] == saved_kwargs[arg]:
  841                 return False
  842         return True
  843 
  844     def compareRemount(self, old_hash_id):
  845         """
  846         Compare mount arguments between current and ``old_hash_id``. If they are
  847         identical we could reuse the mount and don't need to remount.
  848 
  849         Args:
  850             old_hash_id (str):  Hash ID of the old mountpoint
  851 
  852         Returns:
  853             bool:               True if the old mountpoint and current are
  854                                 identiacal
  855         """
  856         if old_hash_id == self.hash_id:
  857             return self.compareUmountInfo(self.umountInfoPath(old_hash_id))
  858         return False
  859 
  860     def setSymlink(self, profile_id = None, hash_id = None, tmp_mount = None):
  861         """
  862         If ``self.symlink`` is ``True`` set symlink
  863         ``~/.local/share/backintime/mnt/<profile id>_<pid>``. Target will be
  864         either the mountpoint or a subfolder of the mountpoint if
  865         ``self.symlink_subfolder`` is set.
  866 
  867         Args:
  868             profile_id (str):   Profile ID that should be linked. If ``None``
  869                                 use ``self.profile_id``
  870             hash_id (str):      Hash ID of mountpoint where this sysmlink should
  871                                 point to. If ``None`` use ``self.hash_id``
  872             tmp_mount (bool):   Set a temporary symlink just for testing new
  873                                 settings
  874         """
  875         if not self.symlink:
  876             return
  877         if profile_id is None:
  878             profile_id = self.profile_id
  879         if hash_id is None:
  880             hash_id = self.hash_id
  881         if tmp_mount is None:
  882             tmp_mount = self.tmp_mount
  883         dst = self.config.snapshotsPath(profile_id = profile_id,
  884                                              mode = self.mode,
  885                                              tmp_mount = tmp_mount)
  886         mountpoint = self.mountpoint(hash_id)
  887         if self.symlink_subfolder is None:
  888             src = mountpoint
  889         else:
  890             src = os.path.join(mountpoint, self.symlink_subfolder)
  891         if os.path.exists(dst):
  892             os.remove(dst)
  893         os.symlink(src, dst)
  894 
  895     def removeSymlink(self, profile_id = None, tmp_mount = None):
  896         """
  897         Remove symlink ``~/.local/share/backintime/mnt/<profile id>_<pid>``
  898 
  899         Args:
  900             profile_id (str):   Profile ID for the symlink
  901             tmp_mount (bool):   Symlink is a temporary link for testing new
  902                                 settings
  903         """
  904         if not self.symlink:
  905             return
  906         if profile_id is None:
  907             profile_id = self.profile_id
  908         if tmp_mount is None:
  909             tmp_mount = self.tmp_mount
  910         os.remove(self.config.snapshotsPath(profile_id = profile_id,
  911                                                  mode = self.mode,
  912                                                  tmp_mount = tmp_mount))
  913 
  914     def hash(self, s):
  915         """
  916         Create a CRC32 hash of string ``s``.
  917 
  918         Args:
  919             s (str):    string that should be hashed
  920 
  921         Returns:
  922             str:        hash of string ``s``
  923         """
  924         return('%X' % (crc32(s.encode()) & 0xFFFFFFFF))
  925 
  926     def hashIdPath(self, hash_id = None):
  927         """
  928         Get path ``~/.local/share/backintime/mnt/<hash_id>``.
  929 
  930         Args:
  931             hash_id (str):  Unique identifier for a mountpoint. If ``None`` use
  932                             ``self.hash_id``
  933 
  934         Returns:
  935             str:            full path to ``<hash_id>``
  936         """
  937         if hash_id is None:
  938             hash_id = self.hash_id
  939         return os.path.join(self.mount_root, self.hash_id)
  940 
  941     def mountpoint(self, hash_id = None):
  942         """
  943         Get path ``~/.local/share/backintime/mnt/<hash_id>/mountpoint``.
  944 
  945         Args:
  946             hash_id (str):  Unique identifier for a mountpoint
  947 
  948         Returns:
  949             str:            full path to ``<hash_id>/mountpoint``
  950         """
  951         return os.path.join(self.hashIdPath(hash_id), 'mountpoint')
  952 
  953     def lockPath(self, hash_id = None):
  954         """
  955         Get path ``~/.local/share/backintime/mnt/<hash_id>/locks``.
  956 
  957         Args:
  958             hash_id (str):  Unique identifier for a mountpoint
  959 
  960         Returns:
  961             str:            full path to ``<hash_id>/locks```
  962         """
  963         return os.path.join(self.hashIdPath(hash_id), 'locks')
  964 
  965     def umountInfoPath(self, hash_id = None):
  966         """
  967         Get path ``~/.local/share/backintime/mnt/<hash_id>/umount``.
  968 
  969         Args:
  970             hash_id (str):  Unique identifier for a mountpoint
  971 
  972         Returns:
  973             str:            full path to ``<hash_id>/umount```
  974         """
  975         return os.path.join(self.hashIdPath(hash_id), 'umount')