#275 Remove the Mock runroot root after the last `run` method of session is done.
Merged 4 years ago by jkaluza. Opened 4 years ago by jkaluza.
jkaluza/odcs mock-runroot  into  master

@@ -49,6 +49,7 @@ 

  import uuid

  import tempfile

  import logging

+ import stat

  

  from odcs.server import conf

  from odcs.server.backend import create_koji_session
@@ -84,6 +85,72 @@ 

          execute_cmd(cmd)

  

  

+ def rmtree_skip_mounts(rootdir, mounts, rootdir_mounts=None):

+     """

+     The rmtree method based on `shutil.rmtree` skipping the `mounts` directories.

+     We want to skip these directories in case the umount fails for whatever reason.

+     We need to ensure that /mnt/odcs content is not removed.

+ 

+     This method catches the `os.error` exceptions, so the runroot task does

+     not fail because of rmtree error.

+ 

+     :param str rootdir: Full path to root directory for Mock chroot to remove.

+         For example "/var/lib/mock/foo/root".

+     :param list mounts: List of mount points to skip in case they are not umounted.

+         For example ["/mnt/odcs", "/mnt/koji"].

+     :param str rootdir_mounts: Helper variable for recursive calls of this

+         function. It is initialized by this function in the first call and is

+         passed to recursive calls. It contains the full-path to mount point.

+         For example:

+             ["/var/lib/mock/foo/root/mnt/odcs", "/var/lib/mock/foo/root/mnt/koji"]

+     :return bool: True if some subdirectory of `rootdir` has been skipped and

+         not removed.

+     """

+     if not rootdir_mounts:

+         rootdir_mounts = ["%s%s" % (rootdir, mount) for mount in mounts]

+ 

+     # Skip the directory which is mount point in case it contains some files -

+     # that means it has not been umounted.

+     # This counts with recursive call of this method. For example:

+     #   rootdir_mounts = ["/var/lib/mock/foo/root/mnt/koji"]

+     #   Call #1:      rootdir = "/var/lib/mock/foo/root/"

+     #     Call #2:    rootdir = "/var/lib/mock/foo/root/mnt"

+     #       Call #3:  rootdir = "/var/lib/mock/foo/root/mnt/koji"

+     # In the Call #3, the rootdir == rootdir_mounts[0], so this directory is

+     # not removed when not empty and True is returned back to calls #2 and #1.

+     # That tells the #2 and #1 to also not remove the "rootdir" with which

+     # they have been called.

+     if rootdir in rootdir_mounts and os.listdir(rootdir):

+         return True

+ 

+     subdirectory_skipped = False

+     names = []

+     try:

+         names = os.listdir(rootdir)

+     except os.error:

+         pass

+     for name in names:

+         fullname = os.path.join(rootdir, name)

+         try:

+             mode = os.lstat(fullname).st_mode

+         except os.error:

+             mode = 0

+         if stat.S_ISDIR(mode):

+             if rmtree_skip_mounts(fullname, mounts, rootdir_mounts):

+                 subdirectory_skipped = True

+         else:

+             try:

+                 os.remove(fullname)

+             except os.error:

+                 continue

+     if not subdirectory_skipped:

+         try:

+             os.rmdir(rootdir)

+         except os.error:

+             pass

+     return subdirectory_skipped

+ 

+ 

  def runroot_tmp_path(runroot_key):

      """

      Creates and returns the temporary path to store the configuration files
@@ -199,10 +266,11 @@ 

      """

      raise_if_runroot_key_invalid(runroot_key)

      rootdir = "/var/lib/mock/%s/root" % runroot_key

+     mounts = [conf.target_dir] + conf.runroot_extra_mounts

  

      try:

          # Mount the conf.targetdir in the Mock chroot.

-         do_mounts(rootdir, [conf.target_dir] + conf.runroot_extra_mounts)

+         do_mounts(rootdir, mounts)

  

          # Wrap the runroot command in /bin/sh, because that's how Koji does

          # that and we need to stay compatible with this way...
@@ -214,7 +282,14 @@ 

          execute_mock(runroot_key, args, False)

      finally:

          # In the end of run, umount the conf.targetdir.

-         undo_mounts(rootdir, [conf.target_dir] + conf.runroot_extra_mounts)

+         undo_mounts(rootdir, mounts)

+         # TODO: Pungi so far does not indicate anyhow that the runroot root can

+         # be removed. The last command is always "rpm -qa ...", so we use it

+         # as a mark for now that runroot root can be removed.

+         # We should try improving Pungi OpenSSH runroot method to remove this

+         # workaround later.

+         if cmd[0] == "rpm":

+             rmtree_skip_mounts(rootdir, mounts)

  

  

  def mock_runroot_main(argv=None):

@@ -21,11 +21,11 @@ 

  # SOFTWARE.

  

  import unittest

- from mock import patch, mock_open

+ from mock import patch, mock_open, MagicMock

  

  from odcs.server.mock_runroot import (

      mock_runroot_init, raise_if_runroot_key_invalid, mock_runroot_run,

-     mock_runroot_install)

+     mock_runroot_install, rmtree_skip_mounts)

  from .utils import AnyStringWith

  

  
@@ -77,3 +77,53 @@ 

      def test_mock_runroot_install(self, execute_mock):

          mock_runroot_install("foo-bar", ["lorax", "dracut"])

          execute_mock.assert_called_once_with('foo-bar', ['--install', 'lorax', 'dracut'])

+ 

+     @patch("odcs.server.mock_runroot.os.rmdir")

+     @patch("odcs.server.mock_runroot.os.listdir")

+     @patch("odcs.server.mock_runroot.os.lstat")

+     @patch("odcs.server.mock_runroot.os.remove")

+     @patch("odcs.server.mock_runroot.stat.S_ISDIR")

+     def test_mock_runroot_rmtree(self, isdir, remove, lstat, listdir, rmdir):

+         """

+         Tests that `rmtree_skip_mounts` really skips the mount points when

+         removing the runroot root directory.

+ 

+         The fake runroot root directory in this test is following:

+         - /mnt/koji - empty directory which should be removed

+         - /mnt/odcs - non-empty mountpoint directory must not be removed.

+         - /x - regular file which should be removed.

+         """

+         def mocked_listdir(path):

+             # Creates fake directory structure within the /var/lib/mock/foo-bar/root:

+             #  - /mnt/koji

+             #  - /mnt/odcs/foo

+             #  - /x

+             if path == "/var/lib/mock/foo-bar/root":

+                 return ["mnt", "x"]

+             if path.endswith("/mnt"):

+                 return ["odcs", "koji"]

+             elif path.endswith("/odcs"):

+                 return ["foo"]

+             return []

+         listdir.side_effect = mocked_listdir

+ 

+         def mocked_isdir(mode):

+             # We use fake values here:

+             # - 0 means it is not a directory.

+             # - 1 means it is a directory.

+             return mode

+         isdir.side_effect = mocked_isdir

+ 

+         def mocked_lstat(path):

+             stat_result = MagicMock()

+             if path.endswith("/x"):

+                 stat_result.st_mode = 0  # Just fake return value for regular file.

+             else:

+                 stat_result.st_mode = 1  # Fake value for directory.

+             return stat_result

+         lstat.side_effect = mocked_lstat

+ 

+         rmtree_skip_mounts("/var/lib/mock/foo-bar/root", ["/mnt/odcs"])

+ 

+         rmdir.assert_called_once_with("/var/lib/mock/foo-bar/root/mnt/koji")

+         remove.assert_called_once_with("/var/lib/mock/foo-bar/root/x")

Without this commit, the old runroot chroots stay in the /var/lib/mock and
are never removed.

Could you document rootdir_mounts parameter?

Aren't rootdir_mounts sub dirs of rootdir?
I'm confused

rebased onto 85a265f

4 years ago

@lucarval, I've tried to add better comment for the confusing part, please check :).

:+1: LGTM!

Thanks for bearing with me :smile:

Pull-Request has been merged by jkaluza

4 years ago