From c4078c160caad8c019c4e890757fbde34aa83f60 Mon Sep 17 00:00:00 2001
From: Jan Kaluza <jkaluza@redhat.com>
Date: Dec 08 2017 13:30:51 +0000
Subject: If pungi_runroot_enabled is true, use koji runroot also to remove expired composes.
---
diff --git a/server/odcs/server/backend.py b/server/odcs/server/backend.py
index c7c77b9..539b23d 100644
--- a/server/odcs/server/backend.py
+++ b/server/odcs/server/backend.py
@@ -143,6 +143,33 @@ class RemoveExpiredComposesThread(BackendThread):
else:
shutil.rmtree(toplevel_dir)
+ def _get_remove_compose_dir_cmds(self, toplevel_dir):
+ """
+ Returns the commands needed to remove the compose with toplevel_dir
+ compose dir as list.
+ """
+
+ # Be nice and don't fail when directory does not exist.
+ if not os.path.exists(toplevel_dir):
+ log.warn("Cannot remove directory %s, it does not exist",
+ toplevel_dir)
+ return []
+
+ cmds = []
+
+ # If toplevel_dir is a symlink, remove the symlink and
+ # its target. If toplevel_dir is normal directory, just
+ # remove it using rmtree.
+ if os.path.realpath(toplevel_dir) != toplevel_dir:
+ targetpath = os.path.realpath(toplevel_dir)
+ cmds += ["rm", "-f", toplevel_dir]
+ if os.path.exists(targetpath):
+ cmds += ["&&", "rm", "-rf", targetpath]
+ else:
+ cmds += ["rm", "-rf", toplevel_dir]
+
+ return cmds
+
def _get_compose_id_from_path(self, path):
"""
Returns the ID of compose from directory path in conf.target_dir.
@@ -163,6 +190,15 @@ class RemoveExpiredComposesThread(BackendThread):
"""
log.info("Checking for expired composes")
+ # We store all the compose directories to remove in this list and
+ # removes them all in the end of this method.
+ # The reason to do it this way is that if "conf.pungi_runroot_enabled"
+ # is True, we spawn Koji runroot task which calls "rm" commands for
+ # every compose directory we want to remove. We need a list
+ # of directories to be able to generate that set of rm commands later
+ # if neede and if not, just remove the directories by this thread.
+ compose_dirs_to_remove = []
+
composes = Compose.composes_to_expire()
for compose in composes:
log.info("%r: Removing compose", compose)
@@ -170,7 +206,7 @@ class RemoveExpiredComposesThread(BackendThread):
compose.time_removed = datetime.utcnow()
db.session.commit()
if not compose.reused_id:
- self._remove_compose_dir(compose.toplevel_dir)
+ compose_dirs_to_remove.append(str(compose.toplevel_dir))
# In case of ODCS error, there might be left-over directories
# belonging to already expired composes. Try to find them in the
@@ -197,16 +233,34 @@ class RemoveExpiredComposesThread(BackendThread):
if not composes:
log.info("Removing data of compose %d - it is not in "
"database: %s", compose_id, path)
- self._remove_compose_dir(path)
+ compose_dirs_to_remove.append(path)
continue
compose = composes[0]
if compose.state == COMPOSE_STATES["removed"]:
log.info("%r: Removing data of compose - it has already "
"expired some time ago: %s", compose_id, path)
- self._remove_compose_dir(path)
+ compose_dirs_to_remove.append(path)
continue
+ koji_cmds = []
+ for compose_dir in compose_dirs_to_remove:
+ if conf.pungi_runroot_enabled:
+ cmds = self._get_remove_compose_dir_cmds(compose_dir)
+ if cmds:
+ koji_cmds += cmds
+ koji_cmds += ["&&"]
+ else:
+ self._remove_compose_dir(compose_dir)
+
+ # In case we have some commands to run in Koji, spawn the Koji runroot
+ # task and wait for result.
+ if koji_cmds:
+ # Remove last "&&"
+ koji_cmds = koji_cmds[:-1]
+ koji_session = odcs.server.utils.make_koji_session()
+ odcs.server.utils.run_koji_runroot(koji_session, koji_cmds)
+
def create_koji_session():
"""
diff --git a/server/odcs/server/pungi.py b/server/odcs/server/pungi.py
index aab57e4..9dbd1d3 100644
--- a/server/odcs/server/pungi.py
+++ b/server/odcs/server/pungi.py
@@ -25,8 +25,6 @@ import os
import shutil
import tempfile
import jinja2
-import koji
-import munch
import time
import random
import string
@@ -35,7 +33,8 @@ import odcs.server.utils
from odcs.server import conf, log
from odcs.server import comps
from odcs.common.types import PungiSourceType, COMPOSE_RESULTS
-from odcs.server.utils import makedirs, download_file
+from odcs.server.utils import (
+ makedirs, download_file, make_koji_session, run_koji_runroot)
class PungiConfig(object):
@@ -185,47 +184,6 @@ class Pungi(object):
output_path = os.path.join(topdir, "pungi.conf")
download_file(self.pungi_cfg, output_path)
- def make_koji_session(self):
- """
- Creates new KojiSession according to odcs.server.conf, logins to
- Koji using this session and returns it.
- :rtype: koji.KojiSession
- :return: KojiSession
- """
- koji_config = munch.Munch(koji.read_config(
- profile_name=conf.koji_profile,
- user_config=conf.koji_config,
- ))
-
- address = koji_config.server
- authtype = koji_config.authtype
- log.info("Connecting to koji %r with %r." % (address, authtype))
- koji_session = koji.ClientSession(address, opts=koji_config)
- if authtype == "kerberos":
- ccache = getattr(conf, "krb_ccache", None)
- keytab = getattr(conf, "krb_keytab", None)
- principal = getattr(conf, "krb_principal", None)
- log.debug(" ccache: %r, keytab: %r, principal: %r" % (
- ccache, keytab, principal))
- if keytab and principal:
- koji_session.krb_login(
- principal=principal,
- keytab=keytab,
- ccache=ccache,
- )
- else:
- koji_session.krb_login(ccache=ccache)
- elif authtype == "ssl":
- koji_session.ssl_login(
- os.path.expanduser(koji_config.cert),
- None,
- os.path.expanduser(koji_config.serverca),
- )
- else:
- raise ValueError("Unrecognized koji authtype %r" % authtype)
-
- return koji_session
-
def get_pungi_cmd(self, conf_topdir, targetdir):
"""
Returns list with pungi command line arguments needed to generate
@@ -305,7 +263,7 @@ class Pungi(object):
makedirs(conf_topdir)
self._write_cfgs(conf_topdir)
- koji_session = self.make_koji_session()
+ koji_session = make_koji_session()
serverdir = self.upload_files_to_koji(koji_session, conf_topdir)
# TODO: Copy keytab from secret repo and generate koji profile.
@@ -313,30 +271,7 @@ class Pungi(object):
cmd += ["cp", "/mnt/koji/work/%s/*" % serverdir, ".", "&&"]
cmd += self.get_pungi_cmd("./", conf.pungi_runroot_target_dir)
- kwargs = {
- 'channel': conf.pungi_parent_runroot_channel,
- 'packages': conf.pungi_parent_runroot_packages,
- 'mounts': conf.pungi_parent_runroot_mounts,
- 'weight': conf.pungi_parent_runroot_weight
- }
-
- task_id = koji_session.runroot(
- conf.pungi_parent_runroot_tag, conf.pungi_parent_runroot_arch,
- " ".join(cmd), **kwargs)
-
- while True:
- # wait for the task to finish
- if koji_session.taskFinished(task_id):
- break
- log.info("Waiting for Koji runroot task %r to finish...", task_id)
- time.sleep(60)
-
- info = koji_session.getTaskInfo(task_id)
- if info is None:
- raise RuntimeError("Cannot get status of Koji task %r" % task_id)
- state = koji.TASK_STATES[info['state']]
- if state in ('FAILED', 'CANCELED'):
- raise RuntimeError("Koji runroot task %r failed." % task_id)
+ run_koji_runroot(koji_session, cmd)
def run(self):
"""
diff --git a/server/odcs/server/utils.py b/server/odcs/server/utils.py
index 63c3574..20e2e50 100644
--- a/server/odcs/server/utils.py
+++ b/server/odcs/server/utils.py
@@ -26,11 +26,86 @@ import os
import time
import subprocess
import requests
+import munch
+import koji
from distutils.spawn import find_executable
from odcs.server import conf, log
+def make_koji_session():
+ """
+ Creates new KojiSession according to odcs.server.conf, logins to
+ Koji using this session and returns it.
+ :rtype: koji.KojiSession
+ :return: KojiSession
+ """
+ koji_config = munch.Munch(koji.read_config(
+ profile_name=conf.koji_profile,
+ user_config=conf.koji_config,
+ ))
+
+ address = koji_config.server
+ authtype = koji_config.authtype
+ log.info("Connecting to koji %r with %r." % (address, authtype))
+ koji_session = koji.ClientSession(address, opts=koji_config)
+ if authtype == "kerberos":
+ ccache = getattr(conf, "krb_ccache", None)
+ keytab = getattr(conf, "krb_keytab", None)
+ principal = getattr(conf, "krb_principal", None)
+ log.debug(" ccache: %r, keytab: %r, principal: %r" % (
+ ccache, keytab, principal))
+ if keytab and principal:
+ koji_session.krb_login(
+ principal=principal,
+ keytab=keytab,
+ ccache=ccache,
+ )
+ else:
+ koji_session.krb_login(ccache=ccache)
+ elif authtype == "ssl":
+ koji_session.ssl_login(
+ os.path.expanduser(koji_config.cert),
+ None,
+ os.path.expanduser(koji_config.serverca),
+ )
+ else:
+ raise ValueError("Unrecognized koji authtype %r" % authtype)
+
+ return koji_session
+
+
+def run_koji_runroot(koji_session, cmd):
+ """
+ Runs the cmd defined as list of commands in Koji runroot and waits for the
+ task to finish. Raises RuntimeError in case the task execution failed.
+ """
+ kwargs = {
+ 'channel': conf.pungi_parent_runroot_channel,
+ 'packages': conf.pungi_parent_runroot_packages,
+ 'mounts': conf.pungi_parent_runroot_mounts,
+ 'weight': conf.pungi_parent_runroot_weight
+ }
+
+ task_id = koji_session.runroot(
+ conf.pungi_parent_runroot_tag, conf.pungi_parent_runroot_arch,
+ " ".join(cmd), **kwargs)
+
+ while True:
+ # wait for the task to finish
+ if koji_session.taskFinished(task_id):
+ break
+ log.info("Waiting for Koji runroot task %r to finish...", task_id)
+ time.sleep(60)
+
+ info = koji_session.getTaskInfo(task_id)
+ if info is None:
+ raise RuntimeError("Cannot get status of Koji task %r" % task_id)
+ state = koji.TASK_STATES[info['state']]
+ if state in ('FAILED', 'CANCELED'):
+ raise RuntimeError("Koji runroot task %r failed." % task_id)
+
+
def download_file(url, output_path):
"""
Downloads file from URL `url` to `output_path`.
diff --git a/server/tests/test_pungi.py b/server/tests/test_pungi.py
index 4d2591d..8709780 100644
--- a/server/tests/test_pungi.py
+++ b/server/tests/test_pungi.py
@@ -201,7 +201,7 @@ class TestPungiRunroot(unittest.TestCase):
self.config_patcher.patch('pungi_runroot_target_dir_url', 'http://kojipkgs.fedoraproject.org/compose/odcs')
self.config_patcher.start()
- self.patch_make_koji_session = patch("odcs.server.pungi.Pungi.make_koji_session")
+ self.patch_make_koji_session = patch("odcs.server.pungi.make_koji_session")
self.make_koji_session = self.patch_make_koji_session.start()
self.koji_session = MagicMock()
self.koji_session.runroot.return_value = 123
diff --git a/server/tests/test_remove_expired_composes_thread.py b/server/tests/test_remove_expired_composes_thread.py
index fb387f7..e4551dc 100644
--- a/server/tests/test_remove_expired_composes_thread.py
+++ b/server/tests/test_remove_expired_composes_thread.py
@@ -21,17 +21,18 @@
# Written by Jan Kaluza <jkaluza@redhat.com>
from odcs.server import db, conf
+import odcs.server.backend
from odcs.server.models import Compose
from odcs.common.types import COMPOSE_STATES, COMPOSE_RESULTS
from odcs.server.backend import RemoveExpiredComposesThread
from odcs.server.pungi import PungiSourceType
from datetime import datetime, timedelta
-from .utils import ModelsBaseTest
+from .utils import ModelsBaseTest, ConfigPatcher
import os
import mock
-from mock import patch
+from mock import patch, MagicMock
class TestRemoveExpiredComposesThread(ModelsBaseTest):
@@ -211,3 +212,149 @@ class TestRemoveExpiredComposesThread(ModelsBaseTest):
self.thread._remove_compose_dir(toplevel_dir)
unlink.assert_not_called()
rmtree.assert_called_once()
+
+
+class TestRemoveExpiredComposesThreadPungiRunroot(ModelsBaseTest):
+ maxDiff = None
+
+ def setUp(self):
+ super(TestRemoveExpiredComposesThreadPungiRunroot, self).setUp()
+
+ compose = Compose.create(
+ db.session, "unknown", PungiSourceType.MODULE, "testmodule-master",
+ COMPOSE_RESULTS["repository"], 60)
+ db.session.add(compose)
+ db.session.commit()
+
+ self.config_patcher = ConfigPatcher(odcs.server.backend.conf)
+ self.config_patcher.patch("pungi_runroot_enabled", True)
+ self.config_patcher.start()
+
+ self.thread = RemoveExpiredComposesThread()
+
+ self.patch_make_koji_session = patch("odcs.server.utils.make_koji_session")
+ self.make_koji_session = self.patch_make_koji_session.start()
+ self.koji_session = MagicMock()
+ self.make_koji_session.return_value = self.koji_session
+
+ self.patch_run_koji_runroot = patch("odcs.server.utils.run_koji_runroot")
+ self.run_koji_runroot = self.patch_run_koji_runroot.start()
+
+ def tearDown(self):
+ super(TestRemoveExpiredComposesThreadPungiRunroot, self).tearDown()
+ self.config_patcher.stop()
+ self.patch_make_koji_session.stop()
+
+ def _mock_glob(self, glob, dirs):
+ glob_ret_values = [[], []]
+ for d in dirs:
+ path = os.path.join(conf.target_dir, d)
+ if d.startswith("latest-"):
+ glob_ret_values[0].append(path)
+ else:
+ glob_ret_values[1].append(path)
+ glob.side_effect = glob_ret_values
+
+ @patch("os.path.isdir")
+ @patch("glob.glob")
+ def test_remove_left_composes(self, glob, isdir):
+ isdir.return_value = True
+ self._mock_glob(glob, ["latest-odcs-96-1", "odcs-96-1-20171005.n.0"])
+ self.thread.do_work()
+
+ self.run_koji_runroot.assert_not_called()
+
+ @patch("os.path.isdir")
+ @patch("glob.glob")
+ def test_remove_left_composes_not_dir(
+ self, glob, isdir):
+ isdir.return_value = False
+ self._mock_glob(glob, ["latest-odcs-96-1"])
+ self.thread.do_work()
+ self.run_koji_runroot.assert_not_called()
+
+ @patch("os.path.isdir")
+ @patch("glob.glob")
+ def test_remove_left_composes_wrong_dir(
+ self, glob, isdir):
+ isdir.return_value = True
+ self._mock_glob(glob, ["latest-odcs-", "odcs-", "odcs-abc"])
+ self.thread.do_work()
+ self.run_koji_runroot.assert_not_called()
+
+ @patch("os.path.isdir")
+ @patch("glob.glob")
+ def test_remove_left_composes_valid_compose(
+ self, glob, isdir):
+ isdir.return_value = True
+ self._mock_glob(glob, ["latest-odcs-1-1", "odcs-1-1-2017.n.0"])
+ c = db.session.query(Compose).filter(Compose.id == 1).one()
+ c.state = COMPOSE_STATES["done"]
+ db.session.add(c)
+ db.session.commit()
+ self.thread.do_work()
+ self.run_koji_runroot.assert_not_called()
+
+ @patch("os.path.isdir")
+ @patch("glob.glob")
+ @patch("os.path.realpath")
+ @patch("os.path.exists")
+ def test_remove_left_composes_expired_compose(
+ self, exists, realpath, glob, isdir):
+ exists.return_value = True
+ realpath.return_value = "/odcs-real"
+ isdir.return_value = True
+ self._mock_glob(glob, ["latest-odcs-1-1", "odcs-1-1-2017.n.0"])
+ c = db.session.query(Compose).filter(Compose.id == 1).one()
+ c.state = COMPOSE_STATES["removed"]
+ db.session.add(c)
+ db.session.commit()
+ self.thread.do_work()
+ self.run_koji_runroot.assert_called_once_with(
+ self.koji_session,
+ ['rm', '-f', os.path.join(conf.target_dir, 'latest-odcs-1-1'), '&&',
+ 'rm', '-rf', '/odcs-real', '&&',
+ 'rm', '-f', os.path.join(conf.target_dir, 'odcs-1-1-2017.n.0'), '&&',
+ 'rm', '-rf', '/odcs-real'])
+
+ @patch("os.path.realpath")
+ @patch("os.path.exists")
+ def test_remove_compose_dir_symlink(
+ self, exists, realpath):
+ exists.return_value = True
+ toplevel_dir = "/odcs"
+ realpath.return_value = "/odcs-real"
+
+ cmds = self.thread._get_remove_compose_dir_cmds(toplevel_dir)
+ self.assertEqual(
+ cmds, ['rm', '-f', '/odcs', '&&', 'rm', '-rf', '/odcs-real'])
+
+ @patch("shutil.rmtree")
+ @patch("os.unlink")
+ @patch("os.path.realpath")
+ @patch("os.path.exists")
+ def test_remove_compose_dir_broken_symlink(
+ self, exists, realpath, unlink, rmtree):
+ def mocked_exists(p):
+ return p != "/odcs-real"
+ exists.side_effect = mocked_exists
+ toplevel_dir = "/odcs"
+ realpath.return_value = "/odcs-real"
+
+ cmds = self.thread._get_remove_compose_dir_cmds(toplevel_dir)
+ self.assertEqual(
+ cmds, ['rm', '-f', '/odcs'])
+
+ @patch("shutil.rmtree")
+ @patch("os.unlink")
+ @patch("os.path.realpath")
+ @patch("os.path.exists")
+ def test_remove_compose_dir_real_dir(
+ self, exists, realpath, unlink, rmtree):
+ exists.return_value = True
+ toplevel_dir = "/odcs"
+ realpath.return_value = "/odcs"
+
+ cmds = self.thread._get_remove_compose_dir_cmds(toplevel_dir)
+ self.assertEqual(
+ cmds, ['rm', '-rf', '/odcs'])