#1385 Simplify backend's worker code
Merged 3 years ago by praiskup. Opened 3 years ago by praiskup.
Unknown source simplify-worker  into  master

file modified
+4
@@ -35,3 +35,7 @@

  

  # Exuberant Ctags

  tags

+ 

+ # External tearballs for unittesting.

+ test-data-*.tar.gz

+ test-data-*/

@@ -0,0 +1,36 @@

+ """

+ Copr enhanced tito.builder.Bulider variant

+ """

+ 

+ import os

+ import urllib.request

+ 

+ from tito.builder import Builder

+ from tito.common import info_out

+ from tito.compat import getoutput

+ 

+ 

+ class CustomBuilder(Builder):

+     """

+     Download Copy extra https:// %{SOURCEX} into the SOURCE folder.

+     """

+ 

+     def copy_extra_sources(self):

+         # C&P from tito/builder.main.py

+         cmd = "spectool -S '%s' --define '_sourcedir %s' | awk '{print $2}'"\

+             % (self.spec_file, self.start_dir)

+         sources = getoutput(cmd).split("\n")

+ 

+         for source in sources[1:]:

+             if not source.startswith("https://"):

+                 # so far we don't have local sources in copr project

+                 continue

+ 

+             target = os.path.join(

+                 self.rpmbuild_sourcedir,

+                 os.path.basename(source),

+             )

+ 

+             # TODO: check md5sum somehow

+             info_out("Downloading %s into %s" % (source, target))

+             urllib.request.urlretrieve(source, target)

file modified
+1 -1
@@ -1,1 +1,1 @@

- 0.38-1 rpmbuild/

+ 0.39-1 rpmbuild/

file modified
+2 -1
@@ -1,5 +1,6 @@

  [buildconfig]

- builder = tito.builder.Builder

+ lib_dir = .tito/library

+ builder = builder.CustomBuilder

  tagger = tito.tagger.VersionTagger

  changelog_do_not_remove_cherrypick = 0

  changelog_format = %s

@@ -13,38 +13,6 @@

  

  dist_git_url=distgitvm.example.com

  

- # Set a number of build groups (default is 1)

- build_groups=1

- 

- # For each build group set:

- #   name - name of the group (will be shown in the worker process name)

- #   archs - architectures to build by this group

- #   spawn_playbook - path to an ansible playbook which spawns a builder

- #   terminate_playbook - path to an ansible playbook to terminate the builder

- #   max_vm_total - maximum number of VM which can run in parallel

- #   max_vm_per_user - maximum number of VM which can use one user in parallel

- #   max_spawn_processes=2 - max number of spawning playbooks run in parallel

- #   vm_spawn_min_interval=30 - after you spin up one VM wait this number of seconds

- #   vm_dirty_terminating_timeout=12 - if user do not reuse VM within this number second then VM is terminated

- #   vm_health_check_period=120 - every X seconds try to check if VM is still alive

- #   vm_health_check_max_time=300 - after this number seconds is not alive it is marked as failed

- #   vm_max_check_fails=2 - when machine is consequently X times marked as failed then it is terminated

- #   vm_terminating_timeout=600 - when machine was terminated and terminate PB did not finish within this number of second, we will run the PB once again.

- #

- #   Use prefix groupX where X is number of group starting from zero.

- #   Warning: any arch should be used once, so no two groups to build the same arch

- # 

- # Example: (and also default values)

- #   group0_name=PC

- #   group0_archs=i386,x86_64

- #   group0_spawn_playbook=/srv/copr-work/provision/builderpb-PC.yml

- #   group0_terminate_playbook=/srv/copr-work/provision/terminatepb-PC.yml

- group0_name=PC

- group0_archs=i386,x86_64

- group0_spawn_playbook=/srv/copr-work/provision/builderpb-PC.yml

- group0_terminate_playbook=/srv/copr-work/provision/terminatepb-PC.yml

- group0_max_vm_total=2

- 

  # directory where results are stored

  # should be accessible from web using 'results_baseurl' URL

  # no default

file modified
+9 -2
@@ -2,6 +2,9 @@

  %global _pkgdocdir %{_docdir}/%{name}-%{version}

  %endif

  

+ %global tests_version 1

+ %global tests_tar test-data-copr-backend

+ 

  Name:       copr-backend

  Version:    1.132

  Release:    1%{?dist}
@@ -13,7 +16,8 @@

  # Source is created by:

  # git clone %%url && cd copr

  # tito build --tgz --tag %%name-%%version-%%release

- Source0:    %name-%version.tar.gz

+ Source0:    %{name}-%{version}.tar.gz

+ Source1:    https://github.com/fedora-copr/%{tests_tar}/archive/v%{tests_version}/%{tests_tar}-%{tests_version}.tar.gz

  

  BuildArch:  noarch

  BuildRequires: asciidoc
@@ -23,6 +27,7 @@

  BuildRequires: libmodulemd >= 1.7.0

  BuildRequires: libxslt

  BuildRequires: redis

+ BuildRequires: rsync

  BuildRequires: systemd

  BuildRequires: util-linux

  
@@ -38,6 +43,7 @@

  BuildRequires: python3-humanize

  BuildRequires: python3-munch

  BuildRequires: python3-oslo-concurrency

+ BuildRequires: python3-packaging

  BuildRequires: python3-pytest

  BuildRequires: python3-pytest-cov

  BuildRequires: python3-pytz
@@ -74,6 +80,7 @@

  Requires:   python3-netaddr

  Requires:   python3-novaclient

  Requires:   python3-oslo-concurrency

+ Requires:   python3-packaging

  Requires:   python3-pytz

  Requires:   python3-requests

  Requires:   python3-resalloc >= 3.0
@@ -106,7 +113,7 @@

  

  

  %prep

- %setup -q

+ %setup -q -a 1

  

  

  %build

@@ -27,14 +27,10 @@

  from .sign import create_user_keys, CoprKeygenRequestError

  from .exceptions import CreateRepoError, CoprSignError, FrontendClientException

  from .helpers import (get_redis_logger, silent_remove, ensure_dir_exists,

-                       get_chroot_arch, cmd_debug, format_filename,

+                       get_chroot_arch, format_filename,

                        uses_devel_repo, call_copr_repo, build_chroot_log_name)

  from .sign import sign_rpms_in_dir, unsign_rpms_in_dir, get_pubkey

  

- from .vm_manage.manager import VmManager

- 

- from .sshcmd import SSHConnectionError, SSHConnection

- 

  

  class Action(object):

      """ Object to send data back to fronted
@@ -69,7 +65,6 @@

              ActionType.RAWHIDE_TO_RELEASE: RawhideToRelease,

              ActionType.FORK: Fork,

              ActionType.BUILD_MODULE: BuildModule,

-             ActionType.CANCEL_BUILD: CancelBuild,

              ActionType.DELETE: Delete,

          }.get(action_type, None)

  
@@ -441,54 +436,6 @@

          return result

  

  

- class CancelBuild(Action):

-     def run(self):

-         data = json.loads(self.data["data"])

-         task_id = data["task_id"]

- 

-         vmm = VmManager(self.opts)

-         vmd = vmm.get_vm_by_task_id(task_id)

-         if vmd:

-             self.log.info("Found VM %s for task %s", vmd.vm_ip, task_id)

-         else:

-             self.log.error("No VM found for task %s", task_id)

-             return ActionResult.FAILURE

- 

-         conn = SSHConnection(

-             user=self.opts.build_user,

-             host=vmd.vm_ip,

-             config_file=self.opts.ssh.builder_config

-         )

- 

-         cmd = "cat /var/lib/copr-rpmbuild/pid"

-         try:

-             rc, out, err = conn.run_expensive(cmd)

-         except SSHConnectionError:

-             self.log.exception("Error running cmd: %s", cmd)

-             return ActionResult.FAILURE

- 

-         cmd_debug(cmd, rc, out, err, self.log)

- 

-         if rc != 0:

-             return ActionResult.FAILURE

- 

-         try:

-             pid = int(out.strip())

-         except ValueError:

-             self.log.exception("Invalid pid %s received", out)

-             return ActionResult.FAILURE

- 

-         cmd = "kill -9 -{}".format(pid)

-         try:

-             rc, out, err = conn.run_expensive(cmd)

-         except SSHConnectionError:

-             self.log.exception("Error running cmd: %s", cmd)

-             return ActionResult.FAILURE

- 

-         cmd_debug(cmd, rc, out, err, self.log)

-         return ActionResult.SUCCESS

- 

- 

  class BuildModule(Action):

      def run(self):

          result = ActionResult.SUCCESS

@@ -35,7 +35,7 @@

              self.log.error("this needs to be run as 'copr' user")

              sys.exit(1)

  

-         self.args = self._get_argparser().parse_args()

+         self.args = self._get_argparser().parse_args(sys.argv[1:])

          be_cfg = self.args.backend_config or '/etc/copr/copr-be.conf'

          self.opts = BackendConfigReader(be_cfg).read()

  
@@ -83,6 +83,11 @@

              help="execute the task on background, as daemon process"

          )

          parser.add_argument(

+             "--silent",

+             action='store_true',

+             help="don't print logs, even when run without --daemon",

+         )

+         parser.add_argument(

              "--backend-config",

              help="alternative path to /etc/copr/copr-be.conf",

          )
@@ -147,7 +152,7 @@

  

          self.log = get_redis_logger(self.opts, logger_name,

                                      self.redis_logger_id)

-         if not self.args.daemon:

+         if not self.args.daemon and not self.args.silent:

              # when executing from commandline - on foreground - we want to

              # print something to stderr as well

              self.log.addHandler(logging.StreamHandler())

@@ -0,0 +1,751 @@

+ """

+ BuildBackgroundWorker class + internals.

+ """

+ 

+ import glob

+ import logging

+ import os

+ import pipes

+ import shutil

+ import statistics

+ import time

+ 

+ from packaging import version

+ 

+ from copr_common.enums import StatusEnum

+ 

+ from copr_backend.background_worker import BackgroundWorker

+ from copr_backend.cancellable_thread import CancellableThreadTask

+ from copr_backend.constants import build_log_format

+ from copr_backend.exceptions import CoprSignError

+ from copr_backend.helpers import (

+     call_copr_repo, pkg_name_evr, run_cmd, register_build_result,

+ )

+ from copr_backend.job import BuildJob

+ from copr_backend.msgbus import MessageSender

+ from copr_backend.sign import sign_rpms_in_dir, get_pubkey

+ from copr_backend.sshcmd import SSHConnection, SSHConnectionError

+ from copr_backend.vm_alloc import ResallocHostFactory

+ 

+ 

+ MAX_HOST_ATTEMPTS = 3

+ MAX_SSH_ATTEMPTS = 5

+ MIN_BUILDER_VERSION = "0.39"

+ CANCEL_CHECK_PERIOD = 5

+ 

+ MESSAGES = {

+     "give_up_repo":

+         "Giving up waiting for copr_base repository, "

+         "please try to manually regenerate the DNF repository",

+     "repo_waiting":

+         "Waiting for copr_base repository",

+     "copr_rpmbuild_missing":

+         "The copr-rpmbuild package was not found: {}",

+ }

+ 

+ COMMANDS = {

+     "rpm_q_builder": "rpm -q copr-rpmbuild --qf \"%{VERSION}\n\"",

+ }

+ 

+ 

+ class BuildRetry(Exception):

+     """

+     Stop processing the build on the current host, and ask for a new

+     one (even though the same may be given by the Resalloc server).

+     So we re-try the build as long as the host seems to reply on SSH

+     channel and it makes at least some sense.  We also retry on at most

+     MAX_HOST_ATTEMPTS hosts when the hosts suddenly become unusable.

+     """

+ 

+ class BuildCanceled(Exception):

+     """ Synchronous cancel request received, fail the build! """

+     def __str__(self):

+         return "Build was canceled"

+ 

+ class BackendError(Exception):

+     """ Generic build failure. """

+     def __str__(self):

+         return "Backend process error: {}".format(super().__str__())

+ 

+ 

+ def _average_step(values):

+     """

+     Calculate average step between ``values``.  It's expected that

+     ``values`` contains at least two items.

+     """

+     if len(values) < 4:

+         return float("inf")

+     previous = None

+     intervals = []

+     for value in values:

+         if previous:

+             intervals.append(value - previous)

+         previous = value

+     return statistics.mean(intervals)

+ 

+ 

+ class LoggingPrivateFilter(logging.Filter):

+     """

+     Filter-out messages that can potentially reveal some data that

+     should stay private.

+     """

+     def filter(self, record):

+         if record.exc_info:

+             traceback = record.exc_info[2]

+             while traceback.tb_next:

+                 traceback = traceback.tb_next

+             fname = traceback.tb_frame.f_code.co_filename

+             fline = traceback.tb_frame.f_lineno

+ 

+             record.exc_info = None

+             record.exc_text = ""

+             record.msg = record.msg + " (in {}:{})".format(

+                 fname, fline,

+             )

+ 

+         return 1

+ 

+ class BuildBackgroundWorker(BackgroundWorker):

+     """

+     The (S)RPM build logic.

+     """

+     # pylint: disable=too-many-instance-attributes

+ 

+     redis_logger_id = 'worker'

+ 

+     def __init__(self):

+         super().__init__()

+         self.sender = None

+         self.builder_pid = None

+         self.builder_dir = "/var/lib/copr-rpmbuild"

+         self.builder_livelog = os.path.join(self.builder_dir, "main.log")

+         self.builder_results = os.path.join(self.builder_dir, "results")

+         self.ssh = None

+         self.job = None

+         self.host = None

+         self.canceled = False

+         self.last_hostname = None

+ 

+     @classmethod

+     def adjust_arg_parser(cls, parser):

+         parser.add_argument(

+             "--build-id",

+             type=int,

+             required=True,

+             help="build ID to process",

+         )

+         parser.add_argument(

+             "--chroot",

+             required=True,

+             help="chroot name (or 'srpm-builds')",

+         )

+ 

+     @property

+     def name(self):

+         """ just a name for logging purposes """

+         return "backend.worker-{}".format(self.worker_id)

+ 

+     def _prepare_result_directory(self, job):

+         """

+         Create backup directory and move there results from previous build.

+         """

+         try:

+             os.makedirs(job.results_dir)

+         except FileExistsError:

+             pass

+ 

+         if not os.listdir(job.results_dir):

+             return

+ 

+         backup_dir_name = "prev_build_backup"

+         backup_dir = os.path.join(job.results_dir, backup_dir_name)

+         self.log.info("Cleaning target directory, results from previous build storing in %s",

+                       backup_dir)

+ 

+         if not os.path.exists(backup_dir):

+             os.makedirs(backup_dir)

+ 

+         files = (x for x in os.listdir(job.results_dir) if x != backup_dir_name)

+         for filename in files:

+             file_path = os.path.join(job.results_dir, filename)

+             if os.path.isfile(file_path):

+                 if file_path.endswith((".info", ".log", ".log.gz")):

+                     os.rename(file_path, os.path.join(backup_dir, filename))

+ 

+                 elif not file_path.endswith(".rpm"):

+                     os.remove(file_path)

+             else:

+                 shutil.rmtree(file_path)

+ 

+     def _setup_resultdir_and_logging(self):

+         """ Prepare the result directory and log file ASAP """

+         self._prepare_result_directory(self.job)

+         handler = logging.handlers.WatchedFileHandler(

+             filename=self.job.backend_log,

+         )

+         handler.setLevel(logging.INFO)

+         handler.setFormatter(build_log_format)

+         handler.addFilter(LoggingPrivateFilter())

+         self.log.addHandler(handler)

+ 

+     def _mark_starting(self):

+         """

+         Announce to the frontend that the build is starting. Frontend may reject

+         build to start.

+         """

+         self.log.info("Marking build as starting")

+         self.job.status = StatusEnum("starting")

+         if not self.frontend_client.starting_build(self.job.to_dict()):

+             raise BackendError("Frontend forbade to start the job {}".format(

+                 self.job.task_id))

+ 

+     def _check_copr_builder(self):

+         rc, out, err = self.ssh.run_expensive(COMMANDS["rpm_q_builder"])

+         if rc != 0:

+             raise BuildRetry(MESSAGES["copr_rpmbuild_missing"].format(err))

+         if version.parse(out) < version.parse(MIN_BUILDER_VERSION):

+             # retry for this issue indefinitely, till the VM is removed and

+             # up2date is spawned

+             raise BuildRetry("Minimum version for builder is {}"

+                              .format(MIN_BUILDER_VERSION))

+ 

+     def _check_mock_config(self):

+         config = "/etc/mock/{}.cfg".format(self.job.chroot)

+         command = "/usr/bin/test -f " + config

+         if self.job.chroot == "srpm-builds":

+             return

+         if self.ssh.run(command):

+             raise BuildRetry("Chroot config {} not found".format(config))

+ 

+     def _check_vm(self):

+         """

+         Check that the VM is OK to start the build

+         """

+         self.log.info("Checking that builder machine is OK")

+         self._check_copr_builder()

+         self._check_mock_config()

+ 

+     def _fill_build_info_file(self):

+         """

+         Places "build.info" which contains job build_id and worker IP

+         into the directory with downloaded files.

+         """

+         info_file_path = os.path.join(self.job.results_dir, "build.info")

+         self.log.info("Filling build.info file with builder info")

+         try:

+             with open(info_file_path, 'w') as info_file:

+                 info_file.writelines([

+                     "build_id={}".format(self.job.build_id),

+                     "\nbuilder_ip={}".format(self.host.hostname)])

+ 

+         except Exception as error:

+             raise BackendError("Can't write to {}: {}".format(

+                 info_file_path, error,

+             ))

+ 

+     def _mark_running(self, attempt):

+         """

+         Announce everywhere that a build process started now.

+         """

+         self._proctitle("Job {}, host info: {}".format(self.job.task_id,

+                                                        self.host.info))

+         self.job.started_on = time.time()

+         self.job.status = StatusEnum("running")

+ 

+         if attempt > 0:

+             # TODO: invent new message type for re-try

+             self.log.info("Not re-notifying FE and msg buses for the new host.")

+             return

+ 

+         self.log.info("Marking build as running on frontend")

+         data = {"builds": [self.job.to_dict()]}

+         self.frontend_client.update(data)

+ 

+         for topic in ['build.start', 'chroot.start']:

+             self.sender.announce(topic, self.job, self.last_hostname)

+ 

+     def _mark_finished(self):

+         self.job.ended_on = time.time()

+ 

+         # At this point, NEVER want to re-try the build by subsequent

+         # BackgroundWorker process.  Let's enforce "finished" state.

+         allowed_states = "failed", "succeeded"

+         if self.job.status not in [StatusEnum(s) for s in allowed_states]:

+             self.log.warning("Switching not-finished job state to 'failed'")

+             self.job.status = StatusEnum("failed")

+ 

+         text_status = StatusEnum(self.job.status)

+         self.log.info("Worker %s build, took %s", text_status,

+                       self.job.took_seconds)

+         data = {"builds": [self.job.to_dict()]}

+         self.frontend_client.update(data)

+         self.sender.announce("build.end", self.job, self.last_hostname)

+ 

+     def _wait_for_repo(self):

+         """

+         Wait a while for initial createrepo, and eventually fail the build

+         if the waiting is not successful.

+         """

+         if self.job.chroot == 'srpm-builds':

+             # we don't need copr_base repodata for srpm builds

+             return

+ 

+         repodata = os.path.join(self.job.chroot_dir, "repodata/repomd.xml")

+         waiting_since = time.time()

+         while time.time() - waiting_since < 60:

+             if os.path.exists(repodata):

+                 return

+ 

+             # Either (a) the very first copr-repo run in this chroot dir

+             # is still running on background (or failed), or (b) we are

+             # hitting the race condition between

+             # 'rm -rf repodata && mv .repodata repodata' sequence that

+             # is done in createrepo_c.  Try again after some time.

+             self.log.info(MESSAGES["repo_waiting"])

+             time.sleep(2)

+ 

+         # This should never happen, but if yes - we need to debug

+         # properly.  Give up waiting, and fail the build.  That should

+         # motivate people to report bugs.

+         raise BackendError(MESSAGES["give_up_repo"])

+ 

+     def _get_build_job(self):

+         """

+         Per self.args, obtain BuildJob instance.

+         """

+         if self.args.chroot == "srpm-builds":

+             target = "get-srpm-build-task/{}".format(self.args.build_id)

+         else:

+             target = "get-build-task/{}-{}".format(self.args.build_id,

+                                                    self.args.chroot)

+         resp = self.frontend_client.get(target)

+         if resp.status_code != 200:

+             self.log.error("Failed to download build info, apache code %s",

+                            resp.status_code)

+             raise BackendError("Failed to get the build task {}".format(target))

+ 

+         self.job = BuildJob(resp.json(), self.opts)

+         self.job.started_on = time.time()

+         if not self.job.chroot:

+             raise BackendError("Frontend job doesn't provide chroot")

+ 

+     def _drop_host(self):

+         """

+         Deallocate assigned host.  We can call this multiple times in row (to

+         make sure the host is deallocated), so this needs to stay idempotent.

+         """

+         if not self.host:

+             return

+ 

+         self.log.info("Releasing VM back to pool")

+         self.host.release()

+         self.host = None

+ 

+     def _proctitle(self, text):

+         text = "Builder for task {}: {}".format(self.job.task_id, text)

+         self.log.debug("setting title: %s", text)

+         self.setproctitle(text)

+ 

+     def _cancel_task_check_request(self):

+         self.canceled = bool(self.redis_get_worker_flag("cancel_request"))

+         return self.canceled

+ 

+     def _cancel_vm_allocation(self):

+         self.redis_set_worker_flag("canceling", 1)

+         self._drop_host()

+ 

+     def _alloc_host(self):

+         """

+         Set self.host with ready RemoteHost, and return True.  Keep re-trying

+         upon allocation failure.  Return False if the request was canceled.

+         """

+         self.log.info("Trying to allocate VM")

+ 

+         tags = []

+         if self.job.arch:

+             tags.append("arch_{}".format(self.job.arch))

+ 

+         vm_factory = ResallocHostFactory(server=self.opts.resalloc_connection)

+         while True:

+             self.host = vm_factory.get_host(tags, self.job.sandbox)

+             self._proctitle("Waiting for VM, info: {}".format(self.host.info))

+             success = CancellableThreadTask(

+                 self.host.wait_ready,

+                 self._cancel_task_check_request,

+                 self._cancel_vm_allocation,

+                 check_period=CANCEL_CHECK_PERIOD,

+             ).run()

+             if self.canceled:

+                 raise BuildCanceled

+             if success:

+                 self.last_hostname = self.host.hostname

+                 return

+             time.sleep(60)

+             self.log.error("VM allocation failed, trying to allocate new VM")

+ 

+     def _alloc_ssh_connection(self):

+         self.log.info("Allocating ssh connection to builder")

+         self.ssh = SSHConnection(

+             user=self.opts.build_user,

+             host=self.host.hostname,

+             config_file=self.opts.ssh.builder_config

+         )

+ 

+     def _cancel_running_worker(self):

+         """

+         This is "canceling" callback to CancellableThreadTask, so please never

+         raise any exception.  The worst case scenario is that nothing is

+         canceled.

+         """

+         self._proctitle("Canceling running task...")

+         self.redis_set_worker_flag("canceling", 1)

+         try:

+             cmd = "copr-rpmbuild-cancel"

+             rc, out, err = self.ssh.run_expensive(cmd, max_retries=3)

+             if rc:

+                 self.log.warning("Can't cancel build\nout:\n%s\nerr:\n%s",

+                                  out, err)

+                 return

+             self.log.info("Cancel request succeeded\nout:\n%serr:\n%s",

+                           out, err)

+         except SSHConnectionError:

+             self.log.error("Can't ssh to cancel build.")

+ 

+     def _start_remote_build(self):

+         """ start the RPM build on builder on background """

+         command = "copr-rpmbuild --verbose --drop-resultdir"

+         if self.job.chroot == "srpm-builds":

+             command += " --srpm --build-id {build_id} --detached"

+         else:

+             command += " --build-id {build_id} --chroot {chroot} --detached"

+         command = command.format(build_id=self.job.build_id,

+                                  chroot=self.job.chroot)

+ 

+         self.log.info("Starting remote build: %s", command)

+         rc, stdout, stderr = self.ssh.run_expensive(command)

+         if rc:

+             raise BackendError("Can't start copr-rpmbuild,\nout:\n{}err:\n{}"

+                                .format(stdout, stderr))

+         try:

+             self.builder_pid = int(stdout.strip())

+         except ValueError:

+             raise BackendError("copr-rpmbuild returned invalid PID "

+                                "on stdout: {}".format(stdout))

+ 

+     def _tail_log_file(self):

+         """ Return None if OK, or failure reason as str """

+         live_cmd = "copr-rpmbuild-log"

+         with open(self.job.builder_log, 'w') as logfile:

+             # We can not use 'max_retries' here because that would concatenate

+             # the attempts to the same log file.

+             if self.ssh.run(live_cmd, stdout=logfile, stderr=logfile):

+                 return "{} shouldn't exit != 0".format(live_cmd)

+         return None

+ 

+     def _retry_for_ssh_failures(self, method, *args, **kwargs):

+         """

+         Retry running the ``method`` indefinitely when SSHConnectionError occurs

+         more frequently than each 2 minutes.

+         """

+         attempt = 0

+         ssh_failures = []

+         while True:

+             attempt += 1

+             try:

+                 self.log.info("Downloading the builder-live.log file, "

+                               "attempt %s", attempt)

+                 return method(*args, **kwargs)

+             except SSHConnectionError as exc:

+                 ssh_failures += [time.time()]

+                 if _average_step(ssh_failures[-4:]) < 120:

+                     self.log.error("Giving up for unstable SSH, failures: %s",

+                                    ", ".join([str(x) for x in ssh_failures]))

+                     raise

+                 sleep = 10

+                 self.log.warning("SSH connection lost on #%s attempt, "

+                                  "let's retry after %ss, %s", attempt, sleep, exc)

+                 time.sleep(sleep)

+                 continue

+ 

+     def _transfer_log_file(self):

+         """

+         Since the tail process can be "watched" for a very long time, there's

+         quite some chance we loose ssh connection in the meantime.  Therefore

+         re-try downloading it till the ssh "looks" to be working.

+ 

+         This is "cancellable" task, so we should NEVER RAISE any exception.

+         """

+         try:

+             return self._retry_for_ssh_failures(self._tail_log_file)

+         except SSHConnectionError as exc:

+             return "Stopped following builder for broken SSH: {}".format(exc)

+ 

+     def _compress_live_logs(self):

+         """

+         Compress builder-live.log and backend.log by gzip.

+         Never raise any exception!

+         """

+         logs = [

+             self.job.builder_log,

+             self.job.backend_log,

+         ]

+ 

+         # For automatic redirect from log to log.gz, consider configuring

+         # lighttpd like:

+         #     url.rewrite-if-not-file = ("^/(.*)/builder-live.log$" => "/$1/redirect-live.log")

+         #     url.redirect("^/(.*)/redirect-live.log$" => "/$1/builder-live.log.gz")

+         # or apache by:

+         #     <Files builder-live.log>

+         #     RewriteEngine on

+         #     RewriteCond %{REQUEST_FILENAME} !-f

+         #     RewriteRule ^(.*)$ %{REQUEST_URI}.gz [R]

+         #     </files>

+ 

+         for src in logs:

+             dest = src + ".gz"

+             if os.path.exists(dest):

+                 # This shouldn't ever happen, but if it happened - gzip below

+                 # would interactively ask whether we want to overwrite the

+                 # existing file, and it would deadlock the worker.

+                 self.log.error("Compressed log %s exists", dest)

+                 continue

+ 

+             self.log.info("Compressing %s by gzip", src)

+             res = run_cmd(["gzip", src])

+             if res.returncode not in [0, 2]:

+                 self.log.error("Unable to compress file %s: %s",

+                                src, res.stderr)

+ 

+     def _download_results(self):

+         """

+         Retry rsync-download the results several times.

+         """

+         self.log.info("Downloading results from builder")

+         self.ssh.rsync_download(

+             self.builder_results + "/",

+             self.job.results_dir,

+             logfile=self.job.rsync_log_name,

+             max_retries=2,

+         )

+ 

+     def _check_build_success(self):

+         """

+         Raise BackendError if builder claims that the build failed.

+         """

+         self.log.info("Searching for 'success' file in resultdir")

+         successfile = os.path.join(self.job.results_dir, "success")

+         if not os.path.exists(successfile):

+             raise BackendError("No success file => build failure")

+ 

+     def _sign_built_packages(self):

+         """

+             Sign built rpms

+              using `copr_username` and `copr_projectname` from self.job

+              by means of obs-sign. If user builds doesn't have a key pair

+              at sign service, it would be created through ``copr-keygen``

+ 

+         :param chroot_dir: Directory with rpms to be signed

+         :param pkg: path to the source package

+ 

+         """

+ 

+         self.log.info("Going to sign pkgs from source: %s in chroot: %s",

+                       self.job.task_id, self.job.chroot_dir)

+ 

+         sign_rpms_in_dir(

+             self.job.project_owner,

+             self.job.project_name,

+             os.path.join(self.job.chroot_dir, self.job.target_dir_name),

+             opts=self.opts,

+             log=self.log

+         )

+ 

+         self.log.info("Sign done")

+ 

+     def _do_createrepo(self):

+         if self.job.chroot == 'srpm-builds':

+             return

+ 

+         project_owner = self.job.project_owner

+         project_name = self.job.project_name

+         devel = self.job.uses_devel_repo

+ 

+         base_url = "/".join([self.opts.results_baseurl, project_owner,

+                              project_name, self.job.chroot])

+ 

+         self.log.info("Incremental createrepo run, adding %s into %s, "

+                       "(auto-create-repo=%s)", self.job.target_dir_name,

+                       base_url, not devel)

+         if not call_copr_repo(self.job.chroot_dir, devel=devel,

+                               add=[self.job.target_dir_name]):

+             raise BackendError("createrepo failed")

+ 

+     def _get_srpm_build_details(self, job):

+         build_details = {'srpm_url': ''}

+         self.log.info("Retrieving srpm URL from %s", job.results_dir)

+         pattern = os.path.join(job.results_dir, '*.src.rpm')

+         srpm_file = glob.glob(pattern)[0]

+         srpm_name = os.path.basename(srpm_file)

+         srpm_url = os.path.join(job.results_dir_url, srpm_name)

+         build_details['pkg_name'], build_details['pkg_version'] = pkg_name_evr(srpm_file)

+         build_details['srpm_url'] = srpm_url

+         self.log.info("SRPM URL: %s", srpm_url)

+         return build_details

+ 

+     def _collect_built_packages(self, job):

+         self.log.info("Listing built binary packages in %s",

+                       job.results_dir)

+ 

+         cmd = (

+             "builtin cd {0} && "

+             "for f in `ls *.rpm | grep -v \"src.rpm$\"`; do"

+             "   rpm -qp --qf \"%{{NAME}} %{{VERSION}}\n\" $f; "

+             "done".format(pipes.quote(job.results_dir))

+         )

+ 

+         result = run_cmd(cmd, shell=True)

+         built_packages = result.stdout.strip()

+         self.log.info("Built packages:\n%s", built_packages)

+         return built_packages

+ 

+ 

+     def _get_build_details(self, job):

+         """

+         :return: dict with build_details

+         :raises BackendError: Something happened with build itself

+         """

+         self.log.info("Getting build details")

+         try:

+             if job.chroot == "srpm-builds":

+                 build_details = self._get_srpm_build_details(job)

+             else:

+                 build_details = {

+                     "built_packages": self._collect_built_packages(job),

+                 }

+             self.log.info("build details: %s", build_details)

+         except Exception as e:

+             raise BackendError(

+                 "Error while collecting built packages for {}: {}"

+                 .format(job.task_id, str(e)))

+ 

+         return build_details

+ 

+     def _add_pubkey(self):

+         """

+         Adds pubkey.gpg with public key to ``chroot_dir`` using

+         ``copr_username`` and ``copr_projectname`` from self.job.

+         """

+         if not self.opts.do_sign:

+             return

+ 

+         self.log.info("Retrieving pubkey")

+ 

+         # TODO: sign repodata as well ?

+         user = self.job.project_owner

+         project = self.job.project_name

+         pubkey_path = os.path.join(self.job.destdir, "pubkey.gpg")

+ 

+         # TODO: uncomment this when key revoke/change will be implemented

+         # if os.path.exists(pubkey_path):

+         #    return

+         get_pubkey(user, project, pubkey_path)

+         self.log.info("Added pubkey for user %s project %s into: %s",

+                       user, project, pubkey_path)

+ 

+     def build(self, attempt):

+         """

+         Attempt to build.

+         """

+         failed = True

+ 

+         self._wait_for_repo()

+         self._alloc_host()

+         self._alloc_ssh_connection()

+         self._check_vm()

+         self._fill_build_info_file()

+         self._mark_running(attempt)

+         self._start_remote_build()

+         transfer_failure = CancellableThreadTask(

+             self._transfer_log_file,

+             self._cancel_task_check_request,

+             self._cancel_running_worker,

+             check_period=CANCEL_CHECK_PERIOD,

+         ).run()

+         if self.canceled:

+             raise BuildCanceled

+         if transfer_failure:

+             raise BuildRetry("SSH problems when downloading live log: {}"

+                              .format(transfer_failure))

+         self._download_results()

+         self._drop_host()

+ 

+         # raise error if build failed

+         try:

+             self._check_build_success()

+             # Build _succeeded_.  Do the tasks for successful run.

+             failed = False

+             if self.opts.do_sign:

+                 self._sign_built_packages()

+             self._do_createrepo()

+             build_details = self._get_build_details(self.job)

+             self.job.update(build_details)

+             self._add_pubkey()

+         except:

+             failed = True

+             raise

+         finally:

+             self.log.info("Finished build: id=%s failed=%s timeout=%s "

+                           "destdir=%s chroot=%s ", self.job.build_id,

+                           failed, self.job.timeout, self.job.destdir,

+                           self.job.chroot)

+             self.job.status = StatusEnum("failed" if failed else "succeeded")

+             register_build_result(self.opts, failed)

+ 

+     def retry_the_build(self):

+         """

+         Indefinitely (at most on MAX_HOST_ATTEMPTS hosts though) retry

+         the build if BuildRetry is raised.

+         """

+         attempt = 0

+         seen_hosts = set()

+         while True:

+             try:

+                 return self.build(attempt)

+             except (BuildRetry, SSHConnectionError) as exc:

+                 seen_hosts.add(self.host.hostname)

+                 attempt += 1

+                 self.log.error("Re-try request for task on '%s': %s",

+                                self.host.info, str(exc))

+                 self._drop_host()

+                 if len(seen_hosts) >= MAX_HOST_ATTEMPTS:

+                     raise BackendError("Three host tried without success: {}"

+                                        .format(seen_hosts))

+                 self.log.info("Retry #%s (on other host)", attempt)

+                 continue

+ 

+     def handle_build(self):

+         """ Do the build """

+         self.sender = MessageSender(self.opts, self.name, self.log)

+         self._get_build_job()

+         self._setup_resultdir_and_logging()

+         self._mark_starting()

+         return self.retry_the_build()

+ 

+     def handle_task(self):

+         """ called by WorkerManager (entry point) """

+         try:

+             self.handle_build()

+         except (BackendError, BuildCanceled) as err:

+             self.log.error(str(err))

+         except CoprSignError as err:

+             self.log.error("Copr GPG signing problems: %s", str(err))

+         except Exception:  # pylint: disable=broad-except

+             self.log.exception("Unexpected exception")

+         finally:

+             self._drop_host()

+             if self.job:

+                 self._mark_finished()

+                 self._compress_live_logs()

+             else:

+                 self.log.error("No job object from Frontend")

+             self.redis_set_worker_flag("status", "done")

@@ -28,9 +28,12 @@

          self.check_period = check_period

          self.log = log or _stderr_logger()

  

-     @staticmethod

-     def _background_run_wrapper(call, result, *args, **kwargs):

-         result.result = call(*args, **kwargs)

+     def _background_run_wrapper(self, call, result, *args, **kwargs):

+         try:

+             result.result = call(*args, **kwargs)

+         except Exception:  # pylint: disable=broad-except

+             # No exceptions to avoid de-synchronization of the threads

+             self.log.exception("Exception during cancellable method")

  

      def run(self, *args, **kwargs):

          """ execute the self.method with args/kwargs """
@@ -50,7 +53,11 @@

  

              if self.check(*args, **kwargs):

                  self.log.debug("calling cancel callback, and waiting")

-                 self.cancel(*args, **kwargs)

+                 try:

+                     self.cancel(*args, **kwargs)

+                 except Exception:  # pylint: disable=broad-except

+                     # No exceptions to avoid de-synchronization of the threads

+                     self.log.exception("Exception during cancel request")

                  thread.join()

                  break

  

@@ -1,228 +0,0 @@

- # coding: utf-8

- 

- from multiprocessing import Process

- import time

- import traceback

- from setproctitle import setproctitle

- 

- 

- from ..vm_manage import VmStates

- from ..exceptions import VmSpawnLimitReached

- 

- from ..helpers import get_redis_logger

- 

- 

- class VmMaster(Process):

-     """

-     Spawns and terminate VM for builder process.

- 

-     :type vmm: backend.vm_manage.manager.VmManager

-     :type spawner: backend.vm_manage.spawn.Spawner

-     :type checker: backend.vm_manage.check.HealthChecker

-     """

-     def __init__(self, opts, vmm, spawner, checker):

-         super(VmMaster, self).__init__(name="vm_master")

- 

-         self.opts = opts

-         self.vmm = vmm

-         self.spawner = spawner

-         self.checker = checker

- 

-         self.kill_received = False

- 

-         self.log = get_redis_logger(self.opts, "vmm.vm_master", "vmm")

-         self.vmm.set_logger(self.log)

- 

-     def remove_old_dirty_vms(self):

-         # terminate vms bound_to user and time.time() - vm.last_release_time > threshold_keep_vm_for_user_timeout

-         #  or add field to VMD ot override common threshold

-         for vmd in self.vmm.get_vm_by_group_and_state_list(None, [VmStates.READY]):

-             if vmd.get_field(self.vmm.rc, "bound_to_user") is None:

-                 continue

-             last_release = vmd.get_field(self.vmm.rc, "last_release")

-             if last_release is None:

-                 continue

-             not_re_acquired_in = time.time() - float(last_release)

-             if not_re_acquired_in > self.opts.build_groups[vmd.group]["vm_dirty_terminating_timeout"]:

-                 self.log.info("dirty VM `%s` not re-acquired in %s, terminating it",

-                               vmd.vm_name, not_re_acquired_in)

-                 self.vmm.start_vm_termination(vmd.vm_name, allowed_pre_state=VmStates.READY)

- 

-     def check_vms_health(self):

-         # for machines in state ready and time.time() - vm.last_health_check > threshold_health_check_period

-         states_to_check = [VmStates.CHECK_HEALTH_FAILED, VmStates.READY,

-                            VmStates.GOT_IP, VmStates.IN_USE]

- 

-         for vmd in self.vmm.get_vm_by_group_and_state_list(None, states_to_check):

-             last_health_check = vmd.get_field(self.vmm.rc, "last_health_check")

-             check_period = self.opts.build_groups[vmd.group]["vm_health_check_period"]

-             if not last_health_check or time.time() - float(last_health_check) > check_period:

-                 self.start_vm_check(vmd.vm_name)

- 

-     def start_vm_check(self, vm_name):

-         """

-         Start VM health check sub-process if current VM state allows it

-         """

- 

-         vmd = self.vmm.get_vm_by_name(vm_name)

-         orig_state = vmd.state

- 

-         if self.vmm.lua_scripts["set_checking_state"](keys=[vmd.vm_key], args=[time.time()]) == "OK":

-             # can start

-             try:

-                 self.checker.run_check_health(vmd.vm_name, vmd.vm_ip)

-             except Exception as err:

-                 self.log.exception("Failed to start health check: %s", err)

-                 if orig_state != VmStates.IN_USE:

-                     vmd.store_field(self.vmm.rc, "state", orig_state)

- 

-         else:

-             self.log.debug("Failed to start vm check, wrong state")

-             return False

- 

-     def _check_total_running_vm_limit(self, group):

-         """ Checks that number of VM in any state excluding Terminating plus

-         number of running spawn processes is less than

-         threshold defined by BackendConfig.build_group[group]["max_vm_total"]

-         """

-         active_vmd_list = self.vmm.get_vm_by_group_and_state_list(

-             group, [VmStates.GOT_IP, VmStates.READY, VmStates.IN_USE,

-                     VmStates.CHECK_HEALTH, VmStates.CHECK_HEALTH_FAILED])

-         total_vm_estimation = len(active_vmd_list) + self.spawner.get_proc_num_per_group(group)

-         if total_vm_estimation >= self.opts.build_groups[group]["max_vm_total"]:

-             raise VmSpawnLimitReached(

-                 "Skip spawn for group {}: max total vm reached: vm count: {}, spawn process: {}"

-                 .format(group, len(active_vmd_list), self.spawner.get_proc_num_per_group(group)))

- 

-     def _check_elapsed_time_after_spawn(self, group):

-         """ Checks that time elapsed since latest VM spawn attempt is greater than

-         threshold defined by BackendConfig.build_group[group]["vm_spawn_min_interval"]

-         """

-         last_vm_spawn_start = self.vmm.read_vm_pool_info(group, "last_vm_spawn_start")

-         if last_vm_spawn_start:

-             time_elapsed = time.time() - float(last_vm_spawn_start)

-             if time_elapsed < self.opts.build_groups[group]["vm_spawn_min_interval"]:

-                 raise VmSpawnLimitReached(

-                     "Skip spawn for group {}: time after previous spawn attempt "

-                     "< vm_spawn_min_interval: {}<{}"

-                     .format(group, time_elapsed, self.opts.build_groups[group]["vm_spawn_min_interval"]))

- 

-     def _check_number_of_running_spawn_processes(self, group):

-         """ Check that number of running spawn processes is less than

-         threshold defined by BackendConfig.build_group[]["max_spawn_processes"]

-         """

-         if self.spawner.get_proc_num_per_group(group) >= self.opts.build_groups[group]["max_spawn_processes"]:

-             raise VmSpawnLimitReached(

-                 "Skip spawn for group {}: reached maximum number of spawning processes: {}"

-                 .format(group, self.spawner.get_proc_num_per_group(group)))

- 

-     def _check_total_vm_limit(self, group):

-         """ Check that number of running spawn processes is less than

-         threshold defined by BackendConfig.build_group[]["max_spawn_processes"]

-         """

-         count_all_vm = len(self.vmm.get_all_vm_in_group(group))

-         if count_all_vm >= 2 * self.opts.build_groups[group]["max_vm_total"]:

-             raise VmSpawnLimitReached(

-                 "Skip spawn for group {}: #(ALL VM) >= 2 * max_vm_total reached: {}"

-                 .format(group, count_all_vm))

- 

-     def try_spawn_one(self, group):

-         """

-         Starts spawning process if all conditions are satisfied

-         """

-         # TODO: add setting "max_vm_in_ready_state", when this number reached, do not spawn more VMS, min value = 1

- 

-         try:

-             self._check_total_running_vm_limit(group)

-             self._check_elapsed_time_after_spawn(group)

-             self._check_number_of_running_spawn_processes(group)

-             self._check_total_vm_limit(group)

-         except VmSpawnLimitReached as err:

-             self.log.debug(err.msg)

-             return

- 

-         self.log.info("Start spawning new VM for group: %s", self.opts.build_groups[group]["name"])

-         self.vmm.write_vm_pool_info(group, "last_vm_spawn_start", time.time())

-         try:

-             self.spawner.start_spawn(group)

-         except Exception as error:

-             self.log.exception("Error during spawn attempt: %s", error)

- 

-     def start_spawn_if_required(self):

-         for group in self.vmm.vm_groups:

-             self.try_spawn_one(group)

- 

-     def do_cycle(self):

-         self.log.debug("starting do_cycle")

- 

-         # TODO: each check should be executed in threads ... and finish with join?

- 

-         self.remove_old_dirty_vms()

-         self.check_vms_health()

-         self.start_spawn_if_required()

- 

-         self.finalize_long_health_checks()

-         self.terminate_again()

- 

-         self.spawner.recycle()

- 

-         # todo: self.terminate_excessive_vms() -- for case when config changed during runtime

- 

-     def run(self):

-         if any(x is None for x in [self.spawner, self.checker]):

-             raise RuntimeError("provide Spawner and HealthChecker "

-                                "to run VmManager daemon")

- 

-         setproctitle("VM master")

-         self.vmm.mark_server_start()

-         self.kill_received = False

- 

-         self.log.info("VM master process started")

-         while not self.kill_received:

-             time.sleep(self.opts.vm_cycle_timeout)

-             try:

-                 self.do_cycle()

-             except Exception as err:

-                 self.log.error("Unhandled error: %s, %s", err, traceback.format_exc())

- 

-     def terminate(self):

-         self.kill_received = True

-         if self.spawner is not None:

-             self.spawner.terminate()

-         if self.checker is not None:

-             self.checker.terminate()

- 

-     def finalize_long_health_checks(self):

-         """

-         After server crash it's possible that some VM's will remain in `check_health` state

-         Here we are looking for such records and mark them with `check_health_failed` state

-         """

-         for vmd in self.vmm.get_vm_by_group_and_state_list(None, [VmStates.CHECK_HEALTH]):

- 

-             time_elapsed = time.time() - float(vmd.get_field(self.vmm.rc, "last_health_check") or 0)

-             if time_elapsed > self.opts.build_groups[vmd.group]["vm_health_check_max_time"]:

-                 self.log.info("VM marked with check fail state, "

-                               "VM stayed too long in health check state, elapsed: %s VM: %s",

-                               time_elapsed, str(vmd))

-                 self.vmm.mark_vm_check_failed(vmd.vm_name)

- 

-     def terminate_again(self):

-         """

-         If we failed to terminate instance request termination once more.

-         Non-terminated instance detected as vm in the `terminating` state with

-             time.time() - `terminating since` > Threshold

-         It's possible, that VM was terminated but termination process doesn't receive confirmation from VM provider,

-         but we have already got a new VM with the same IP => it's safe to remove old vm from pool

-         """

- 

-         for vmd in self.vmm.get_vm_by_group_and_state_list(None, [VmStates.TERMINATING]):

-             time_elapsed = time.time() - float(vmd.get_field(self.vmm.rc, "terminating_since") or 0)

-             if time_elapsed > self.opts.build_groups[vmd.group]["vm_terminating_timeout"]:

-                 if len(self.vmm.lookup_vms_by_ip(vmd.vm_ip)) > 1:

-                     self.log.info(

-                         "Removing VM record: %s. There are more VM with the same ip, "

-                         "it's safe to remove current one from VM pool", vmd.vm_name)

-                     self.vmm.remove_vm_from_pool(vmd.vm_name)

-                 else:

-                     self.log.info("Sent VM %s for termination again", vmd.vm_name)

-                     self.vmm.start_vm_termination(vmd.vm_name, allowed_pre_state=VmStates.TERMINATING)

@@ -1,379 +0,0 @@

- import os

- import time

- import gzip

- import shutil

- import pipes

- import glob

- 

- from ..exceptions import MockRemoteError, CoprWorkerError, VmError, CoprBackendSrpmError

- from ..mockremote import MockRemote

- from ..constants import BuildStatus, build_log_format

- from ..helpers import register_build_result, get_redis_logger, \

-     local_file_logger, run_cmd, pkg_name_evr

- from ..frontend import FrontendClient

- 

- from ..msgbus import (

-         MsgBusStomp,

-         MsgBusFedmsg,

-         MsgBusFedoraMessaging,

- )

- from ..sshcmd import SSHConnectionError

- 

- 

- # ansible_playbook = "ansible-playbook"

- 

- class Worker:

-     """

-     Do the rpm build.  TODO: move from daemons/ directory.

-     """

-     msg_buses = []

- 

-     def __init__(self, opts, worker_id, vm, job):

-         self.pid = os.getpid()

- 

-         # safe variables to be inherited through fork() call

-         self.opts = opts

-         self.worker_id = worker_id

-         self.vm = vm

-         self.job = job

- 

-         # for the sake of readability, those are defined in run()

-         self.log = None

-         self.frontend_client = None

- 

-     @property

-     def name(self):

-         """ just a name for logging purposes """

-         return "backend.worker-{}".format(self.worker_id)

- 

-     def _announce(self, topic, job):

-         for bus in self.msg_buses:

-             bus.announce_job(

-                 topic, job,

-                 who=self.name,

-                 ip=self.vm.hostname,

-                 pid=self.pid

-             )

- 

-     def _announce_start(self, job):

-         """

-         Announce everywhere that a build process started now.

-         """

-         job.started_on = time.time()

-         self._mark_running(job)

- 

-         for topic in ['build.start', 'chroot.start']:

-             self._announce(topic, job)

- 

- 

-     def _announce_end(self, job):

-         """

-         Announce everywhere that a build process ended now.

-         """

-         job.ended_on = time.time()

-         self.return_results(job)

-         self.log.info("worker finished build: %s", self.vm.hostname)

-         self._announce('build.end', job)

- 

-     def _mark_running(self, job):

-         """

-         Send data about started build to the frontend

-         """

-         job.status = BuildStatus.RUNNING

-         build = job.to_dict()

-         self.log.info("starting build: %s", build)

- 

-         data = {"builds": [build]}

-         try:

-             self.frontend_client.update(data)

-         except:

-             raise CoprWorkerError("Could not communicate to front end to submit status info")

- 

-     def return_results(self, job):

-         """

-         Send the build results to the frontend

-         """

-         self.log.info("Build %s finished with status %s",

-                       job.build_id, job.status)

- 

-         self.log.info("Took %s seconds.", job.ended_on - job.started_on)

- 

-         data = {"builds": [job.to_dict()]}

- 

-         try:

-             self.frontend_client.update(data)

-         except Exception as err:

-             raise CoprWorkerError(

-                 "Could not communicate to front end to submit results: {}"

-                 .format(err)

-             )

- 

-     @classmethod

-     def pkg_built_before(cls, pkg, chroot, destdir):

-         """

-         Check whether the package has already been built in this chroot.

-         """

-         s_pkg = os.path.basename(pkg)

-         pdn = s_pkg.replace(".src.rpm", "")

-         resdir = "{0}/{1}/{2}".format(destdir, chroot, pdn)

-         resdir = os.path.normpath(resdir)

-         if os.path.exists(resdir) and os.path.exists(os.path.join(resdir, "success")):

-             return True

-         return False

- 

-     def init_buses(self):

-         for bus_config in self.opts.msg_buses:

-             if bus_config.bus_type == 'stomp':

-                 self.msg_buses.append(MsgBusStomp(bus_config, self.log))

-             elif bus_config.bus_type == 'fedora-messaging':

-                 self.msg_buses.append(MsgBusFedoraMessaging(bus_config, self.log))

- 

-         if self.opts.fedmsg_enabled:

-             self.msg_buses.append(MsgBusFedmsg(self.log))

- 

-     # TODO: doing skip logic on fronted during @start_build query

-     # def on_pkg_skip(self, job):

-     #     """

-     #     Handle package skip

-     #     """

-     #     self._announce_start(job)

-     #     self.log.info("Skipping: package {} has been already built before.".format(job.pkg))

-     #     job.status = BuildStatus.SKIPPED

-     #     self._announce_end(job)

- 

-     def wait_for_repo(self, job):

-         if job.chroot == 'srpm-builds':

-             # we don't need copr_base repodata for srpm builds

-             return True

- 

-         repodata = os.path.join(job.chroot_dir, "repodata/repomd.xml")

-         waiting_from = time.time()

-         while time.time() - waiting_from < 60:

-             if os.path.exists(repodata):

-                 return True

- 

-             # Either (a) the very first copr-repo run in this chroot dir

-             # is still running on background (or failed), or (b) we are

-             # hitting the race condition between

-             # 'rm -rf repodata && mv .repodata repodata' sequence that

-             # is done in createrepo_c.  Try again after some time.

-             self.log.info("waiting for copr_base repository")

-             time.sleep(2)

- 

-         self.log.error("giving up waiting for copr_base repository")

- 

-         # This should never happen, but if yes - we need to debug

-         # properly.  Give up waiting, and fail the build.  That should

-         # motivate people to report bugs.

-         return False

- 

-     def do_job(self, job):

-         """

-         Executes new job.

- 

-         :param job: :py:class:`~backend.job.BuildJob`

-         """

-         failed = False

- 

-         self._announce_start(job)

- 

-         # setup our target dir locally

-         if not os.path.exists(job.chroot_dir):

-             try:

-                 os.makedirs(job.chroot_dir)

-             except (OSError, IOError):

-                 self.log.exception("Could not make results dir for job: %s",

-                                    job.chroot_dir)

-                 failed = True

- 

-         if not self.wait_for_repo(job):

-             failed = True

-         self.prepare_result_directory(job)

- 

-         if not failed:

-             # FIXME

-             # need a plugin hook or some mechanism to check random

-             # info about the pkgs

-             # this should use ansible to download the pkg on

-             # the remote system

-             # and run a series of checks on the package before we

-             # start the build - most importantly license checks.

- 

-             self.log.info("Starting build: id=%s builder=%s job: %s",

-                           job.build_id, self.vm.hostname, job)

- 

-             with local_file_logger(

-                 "{}.builder.mr".format(self.name),

-                 job.chroot_log_path,

-                 fmt=build_log_format) as build_logger:

- 

-                 try:

-                     mr = MockRemote(

-                         builder_host=self.vm.hostname,

-                         job=job,

-                         logger=build_logger,

-                         opts=self.opts

-                     )

- 

-                     mr.check()

-                     mr.build_pkg()

-                     mr.compress_live_log(job)

-                     mr.check_build_success() # raises if build didn't succeed

-                     mr.download_results()

- 

-                 except MockRemoteError as e:

-                     # record and break

-                     self.log.error(

-                         "Error during the build, host=%s, build_id=%s, "

-                         "chroot=%s, error='%s'",

-                         self.vm.hostname, job.build_id, job.chroot, str(e))

-                     failed = True

-                     mr.download_results()

- 

-                 except SSHConnectionError as err:

-                     self.log.exception(

-                         "SSH connection stalled: %s", str(err))

-                     self.vm.release()

-                     self.frontend_client.reschedule_build(

-                         job.build_id, job.task_id, job.chroot)

-                     raise VmError("SSH connection issue, build rescheduled")

- 

-                 except: # programmer's failure

-                     self.log.exception("Unexpected error")

-                     failed = True

- 

-                 if not failed:

-                     try:

-                         mr.on_success_build()

-                         build_details = self.get_build_details(job)

-                         job.update(build_details)

- 

-                         if self.opts.do_sign:

-                             mr.add_pubkey()

-                     except:

-                         self.log.exception("Error during backend post-build processing.")

-                         failed = True

- 

-             self.log.info(

-                 "Finished build: id=%s builder=%s timeout=%s destdir=%s chroot=%s",

-                 job.build_id, self.vm.hostname, job.timeout, job.destdir, job.chroot)

-             self.copy_logs(job)

- 

-         register_build_result(self.opts, failed=failed)

-         job.status = (BuildStatus.FAILURE if failed else BuildStatus.SUCCEEDED)

- 

-         self._announce_end(job)

- 

-     def collect_built_packages(self, job):

-         self.log.info("Listing built binary packages in %s",

-                       job.results_dir)

- 

-         cmd = (

-             "builtin cd {0} && "

-             "for f in `ls *.rpm | grep -v \"src.rpm$\"`; do"

-             "   rpm -qp --qf \"%{{NAME}} %{{VERSION}}\n\" $f; "

-             "done".format(pipes.quote(job.results_dir))

-         )

-         result = run_cmd(cmd, shell=True)

-         built_packages = result.stdout.strip()

-         self.log.info("Built packages:\n%s", built_packages)

-         return built_packages

- 

-     def get_srpm_build_details(self, job):

-         build_details = {'srpm_url': ''}

-         self.log.info("Retrieving srpm URL from %s", job.results_dir)

-         pattern = os.path.join(job.results_dir, '*.src.rpm')

-         srpm_file = glob.glob(pattern)[0]

-         srpm_name = os.path.basename(srpm_file)

-         srpm_url = os.path.join(job.results_dir_url, srpm_name)

-         build_details['pkg_name'], build_details['pkg_version'] = pkg_name_evr(srpm_file)

-         build_details['srpm_url'] = srpm_url

-         self.log.info("SRPM URL: %s", srpm_url)

-         return build_details

- 

-     def get_build_details(self, job):

-         """

-         :return: dict with build_details

-         :raises MockRemoteError: Something happened with build itself

-         """

-         try:

-             if job.chroot == "srpm-builds":

-                 build_details = self.get_srpm_build_details(job)

-             else:

-                 build_details = { "built_packages": self.collect_built_packages(job) }

-             self.log.info("build details: %s", build_details)

-         except Exception as e:

-             self.log.exception(str(e))

-             raise CoprWorkerError("Error while collecting built packages for {}.".format(job))

- 

-         return build_details

- 

-     def copy_logs(self, job):

-         if not os.path.isdir(job.results_dir):

-             self.log.info("Job results dir doesn't exists, couldn't copy main log; path: %s",

-                           job.results_dir)

-             return

- 

-         logs_to_copy = [

-             (os.path.join(job.chroot_log_path),

-              os.path.join(job.results_dir, "backend.log.gz"))

-         ]

- 

-         for src, dst in logs_to_copy:

-             try:

-                 with open(src, "rb") as f_src, gzip.open(dst, "wb") as f_dst:

-                     f_dst.writelines(f_src)

-             except IOError:

-                 self.log.info("File %s not found", src)

- 

-     def prepare_result_directory(self, job):

-         """

-         Create backup directory and move there results from previous build.

-         """

-         try:

-             os.mkdir(job.results_dir)

-         except FileExistsError:

-             pass

- 

-         if not os.listdir(job.results_dir):

-             return

- 

-         backup_dir_name = "prev_build_backup"

-         backup_dir = os.path.join(job.results_dir, backup_dir_name)

-         self.log.info("Cleaning target directory, results from previous build storing in %s",

-                       backup_dir)

- 

-         if not os.path.exists(backup_dir):

-             os.makedirs(backup_dir)

- 

-         files = (x for x in os.listdir(job.results_dir) if x != backup_dir_name)

-         for filename in files:

-             file_path = os.path.join(job.results_dir, filename)

-             if os.path.isfile(file_path):

-                 if file_path.endswith((".info", ".log", ".log.gz")):

-                     os.rename(file_path, os.path.join(backup_dir, filename))

- 

-                 elif not file_path.endswith(".rpm"):

-                     os.remove(file_path)

-             else:

-                 shutil.rmtree(file_path)

- 

-     def run(self):

-         """

-         Run the worker (in sync).  This method exists because previously we

-         inherited from multiprocessing.Process class.

-         """

-         self.log = get_redis_logger(self.opts, self.name, "worker")

-         self.frontend_client = FrontendClient(self.opts, self.log,

-                                               try_indefinitely=True)

-         self.log.info("Starting worker")

-         self.init_buses()

- 

-         try:

-             self.do_job(self.job)

-         except VmError as error:

-             self.log.exception("Building error: %s", error)

-         except Exception as e:

-             self.log.exception("Unexpected error: %s", e)

-         finally:

-             self.vm.release()

@@ -1,17 +1,4 @@

- class MockRemoteError(Exception):

-     pass

- 

- class BuilderError(MockRemoteError):

-     pass

- 

- class RemoteCmdError(BuilderError):

-     def __init__(self, msg, cmd, rc, stderr, stdout):

-         self.msg = "{}\nCMD:{}\nRC:{}\nSTDERR:{}\nSTDOUT:{}".format(

-             msg, cmd, rc, stderr, stdout

-         )

-         super(RemoteCmdError, self).__init__(self.msg)

- 

- class CoprSignError(MockRemoteError):

+ class CoprSignError(Exception):

      """

      Related to invocation of /bin/sign

  
@@ -45,7 +32,7 @@

      pass

  

  

- class CoprKeygenRequestError(MockRemoteError):

+ class CoprKeygenRequestError(Exception):

      """

      Errors during request to copr-keygen service

  

@@ -434,6 +434,12 @@

  

      def emit(self, record):

          # copr specific semantics

+ 

+         # Alternative to copy.deepcopy().  If we edit the original record

+         # object, any other following log handler would get the modified

+         # variant.

+         record = logging.makeLogRecord(record.__dict__)

+ 

          record.who = self.who

  

          # First argument to 'log.exception()' should be 'str' type.  If it is
@@ -559,7 +565,7 @@

          raise CoprBackendSrpmError(str(e))

  

      if result.returncode != 0:

-         raise CoprBackendSrpmError('Error querying srpm: %s' % error)

+         raise CoprBackendSrpmError('Error querying srpm: %s' % result.stderr)

  

      try:

          name, epoch, version, release = result.stdout.split(" ")

file modified
+22 -5
@@ -1,7 +1,7 @@

  import copy

  import os

  

- from copr_backend.helpers import build_target_dir, build_chroot_log_name

+ from copr_backend.helpers import build_target_dir

  

  

  class BuildJob(object):
@@ -89,6 +89,9 @@

              task_data["project_dirname"],

          )

  

+         # TODO: We should rename this attribute.  This one is used by Frontend

+         # to store updated "target_dir_name" to BuildChroot database.  But the

+         # name is terrible, and clashes with self.results_dir (plural).

          self.result_dir = self.target_dir_name

  

          self.built_packages = ""
@@ -110,12 +113,19 @@

          return build_target_dir(self.build_id, self.package_name)

  

      @property

-     def chroot_log_name(self):

-         return build_chroot_log_name(self.build_id, self.package_name)

+     def backend_log(self):

+         """

+         The log file which is "live" appended to build resultdir by copr

+         backend background process.

+         """

+         return os.path.join(self.results_dir, "backend.log")

  

      @property

-     def chroot_log_path(self):

-         return os.path.join(self.results_dir, self.chroot_log_name)

+     def builder_log(self):

+         """

+         The live log continuously transferred from builder.

+         """

+         return os.path.join(self.results_dir, "builder-live.log")

  

      @property

      def rsync_log_name(self):
@@ -171,3 +181,10 @@

      def __unicode__(self):

          return u"BuildJob<id: {build_id}, owner: {project_owner}, project: {project_name}, project_dir: {project_dirname}" \

                 u"git branch: {git_branch}, git hash: {git_hash}, status: {status} >".format(**self.__dict__)

+ 

+     @property

+     def took_seconds(self):

+         """ Number of seconds spent on building this package """

+         if self.ended_on is None or self.started_on is None:

+             return None

+         return self.ended_on - self.started_on

@@ -1,330 +0,0 @@

- # by skvidal

- # This program is free software; you can redistribute it and/or modify

- # it under the terms of the GNU General Public License as published by

- # the Free Software Foundation; either version 2 of the License, or

- # (at your option) any later version.

- #

- # This program is distributed in the hope that it will be useful,

- # but WITHOUT ANY WARRANTY; without even the implied warranty of

- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

- # GNU Library General Public License for more details.

- #

- # You should have received a copy of the GNU General Public License

- # along with this program; if not, write to the Free Software

- # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.

- # copyright 2012 Red Hat, Inc.

- 

- import fcntl

- import logging

- import os

- import subprocess

- 

- from munch import Munch

- import time

- 

- from ..constants import DEF_BUILD_TIMEOUT, DEF_REPOS, \

-     DEF_BUILD_USER, DEF_MACROS

- from ..exceptions import MockRemoteError, BuilderError

- from ..helpers import uses_devel_repo, call_copr_repo

- 

- 

- # TODO: replace sign & createrepo with dependency injection

- from ..sign import sign_rpms_in_dir, get_pubkey

- 

- from .builder import Builder, SrpmBuilder

- 

- 

- # class BuilderThread(object):

- #     def __init__(self, builder_obj):

- #         self.builder = builder_obj

- #         self._running = False

- #

- #     def terminate(self):

- #         self._running = False

- #

- #     def run(self):

- #         self.builder.start_build()

- #         self._running = True

- #         state = None

- #         while self._running:

- #             state = self.builder.update_progress()

- #             if state in ["done", "failed"]:

- #                 self._running = False

- #             else:

- #                 time.sleep(5)

- #

- #         if state == "done":

- #             self.builder.after_build()

- 

- 

- class MockRemote(object):

-     # TODO: Refactor me!

-     #   mock remote now do too much things

-     #   idea: send events according to the build progress to handler

- 

-     def __init__(self, builder_host, job, logger,

-                  opts=None):

- 

-         """

-         :param builder_host: builder hostname or ip

- 

-         :param backend.job.BuildJob job: Job object with the following attributes::

-             :ivar timeout: ssh timeout

-             :ivar destdir: target directory to put built packages

-             :ivar chroot: chroot config name/base to use in the mock build

-                            (e.g.: fedora20_i386 )

-             :ivar build_id: copr build.id

-             :ivar pkg: pkg to build

- 

- 

-         :param macros: {    "copr_username": ...,

-                             "copr_projectname": ...,

-                             "vendor": ...}

- 

-         :param Munch opts: builder options, used keys::

-             :ivar build_user: user to run as/connect as on builder systems

-             :ivar do_sign: enable package signing, require configured

-                 signer host and correct /etc/sign.conf

-             :ivar frontend_base_url: url to the copr frontend

-             :ivar results_baseurl: base url for the built results

- 

-         # Removed:

-         # :param cont: if a pkg fails to build, continue to the next one--

-         # :param bool recurse: if more than one pkg and it fails to build,

-         #                      try to build the rest and come back to it

-         """

-         self.opts = Munch(

-             do_sign=False,

-             frontend_base_url=None,

-             results_baseurl=u"",

-             build_user=DEF_BUILD_USER,

-             timeout=DEF_BUILD_TIMEOUT,

-         )

-         if opts:

-             self.opts.update(opts)

- 

-         self.log = logger

-         self.job = job

- 

-         self.log.info("Setting up builder: {0}".format(builder_host))

-         # TODO: add option "builder_log_level" to backend config

-         self.log.setLevel(logging.INFO)

- 

-         builder_cls = (SrpmBuilder if self.job.chroot == 'srpm-builds' else Builder)

- 

-         self.builder = builder_cls(

-             opts=self.opts,

-             hostname=builder_host,

-             job=self.job,

-             logger=logger,

-         )

- 

-         self.failed = []

-         self.finished = []

- 

-     def check(self):

-         """

-         Checks that MockRemote configuration and environment are correct.

- 

-         :raises MockRemoteError: when configuration is wrong or

-             some expected resource is unavailable

-         """

-         if not self.job.chroot:

-             raise MockRemoteError("No chroot specified!")

- 

-         try:

-             self.builder.check()

-         except BuilderError as error:

-             raise MockRemoteError(str(error))

- 

-     @property

-     def chroot_dir(self):

-         return os.path.normpath(os.path.join(self.job.destdir, self.job.chroot))

- 

-     @property

-     def pkg(self):

-         return self.job.pkg

- 

-     def add_pubkey(self):

-         """

-             Adds pubkey.gpg with public key to ``chroot_dir``

-             using `copr_username` and `copr_projectname` from self.job.

-         """

-         self.log.info("Retrieving pubkey ")

-         # TODO: sign repodata as well ?

-         user = self.job.project_owner

-         project = self.job.project_name

-         pubkey_path = os.path.join(self.job.destdir, "pubkey.gpg")

-         try:

-             # TODO: uncomment this when key revoke/change will be implemented

-             # if os.path.exists(pubkey_path):

-             #    return

- 

-             get_pubkey(user, project, pubkey_path)

-             self.log.info(

-                 "Added pubkey for user {} project {} into: {}".

-                 format(user, project, pubkey_path))

- 

-         except Exception as e:

-             self.log.exception(

-                 "failed to retrieve pubkey for user {} project {} due to: \n"

-                 "{}".format(user, project, e))

- 

-     def sign_built_packages(self):

-         """

-             Sign built rpms

-              using `copr_username` and `copr_projectname` from self.job

-              by means of obs-sign. If user builds doesn't have a key pair

-              at sign service, it would be created through ``copr-keygen``

- 

-         :param chroot_dir: Directory with rpms to be signed

-         :param pkg: path to the source package

- 

-         """

- 

-         self.log.info("Going to sign pkgs from source: {} in chroot: {}"

-                       .format(self.job, self.chroot_dir))

- 

-         try:

-             sign_rpms_in_dir(

-                 self.job.project_owner,

-                 self.job.project_name,

-                 os.path.join(self.chroot_dir, self.job.target_dir_name),

-                 opts=self.opts,

-                 log=self.log

-             )

-         except Exception as e:

-             self.log.exception(

-                 "failed to sign packages built from `{}` with error".format(self.job))

-             if isinstance(e, MockRemoteError):

-                 raise

- 

-         self.log.info("Sign done")

- 

-     def do_createrepo(self):

-         if self.job.chroot == 'srpm-builds':

-             return

- 

-         project_owner = self.job.project_owner

-         project_name = self.job.project_name

-         devel = self.job.uses_devel_repo

- 

-         base_url = "/".join([self.opts.results_baseurl, project_owner,

-                              project_name, self.job.chroot])

-         self.log.info("Createrepo:: owner:  {}; project: {}; "

-                       "front url: {}; path: {}; base_url: {}"

-                       .format(project_owner, project_name,

-                               self.opts.frontend_base_url, self.chroot_dir, base_url))

- 

-         if not call_copr_repo(self.chroot_dir, devel=devel,

-                               add=[self.job.target_dir_name]):

-             raise MockRemoteError("do_createrepo failed")

- 

-     def on_success_build(self):

-         self.log.info("Success building {0}".format(self.job.package_name))

- 

-         if self.opts.do_sign:

-             self.sign_built_packages()

- 

-         # createrepo with the new pkgs

-         self.do_createrepo()

- 

-     def prepare_build_dir(self):

-         p_path = self.job.results_dir

-         # if it's marked as fail, nuke the failure and try to rebuild it

-         if os.path.exists(os.path.join(p_path, "fail")):

-             os.unlink(os.path.join(p_path, "fail"))

- 

-         if os.path.exists(os.path.join(p_path, "success")):

-             os.unlink(os.path.join(p_path, "success"))

- 

-         # mkdir to download results

-         if not os.path.exists(p_path):

-             os.makedirs(p_path)

- 

-         self.mark_dir_with_build_id()

- 

-     # def add_log_symlinks(self):

-     #     # adding symlinks foo.log.gz -> foo.log for nginx web server auto index

-     #     try:

-     #         base = self._get_pkg_destpath()

-     #         for name in os.listdir(base):

-     #             if not name.endswith(".log.gz"):

-     #                 continue

-     #             full_path = os.path.join(base, name)

-     #             if os.path.isfile(full_path):

-     #                 os.symlink(full_path, full_path.replace(".log.gz", ".log"))

-     #     except Exception as err:

-     #         self.log.exception(err)

- 

-     def build_pkg(self):

-         self.log.info("Start build: {}".format(self.job))

- 

-         self.prepare_build_dir()

- 

-         try:

-             self.builder.build()

-         except BuilderError as error:

-             self.log.error(str(error))

-             raise MockRemoteError("Builder error during job {}.".format(self.job))

- 

-     def compress_live_log(self, job):

-         log_basename = "builder-live.log"

-         src = os.path.join(job.results_dir, log_basename)

- 

-         # For automatic redirect from log to log.gz, consider configuring

-         # lighttpd like:

-         #     url.rewrite-if-not-file = ("^/(.*)/builder-live.log$" => "/$1/redirect-live.log")

-         #     url.redirect("^/(.*)/redirect-live.log$" => "/$1/builder-live.log.gz")

-         # or apache by:

-         #     <Files builder-live.log>

-         #     RewriteEngine on

-         #     RewriteCond %{REQUEST_FILENAME} !-f

-         #     RewriteRule ^(.*)$ %{REQUEST_URI}.gz [R]

-         #     </files>

-         self.log.info("Compressing {} by gzip".format(src))

-         if subprocess.call(["gzip", src]) not in [0, 2]:

-             self.log.error("Unable to compress file {}".format(src))

-             return

- 

-     def reattach_to_pkg_build(self):

-         """

-         :raises VmError, BuilderError

-         """

-         self.log.info("Reattach to build: {}".format(self.job))

- 

-         try:

-             self.builder.reattach()

-         except BuilderError as error:

-             self.log.error(str(error))

-             raise MockRemoteError("Builder error during job {}.".format(self.job))

- 

-     def check_build_success(self):

-         """

-         Raise MockRemoteError if builder claims that the build failed.

-         """

-         if not self.builder.check_build_success():

-             raise MockRemoteError("Build {} failed".format(self.job))

- 

-     def download_results(self):

-         try:

-             self.builder.download_results(self.job.results_dir)

-         except Exception as err:

-             self.log.exception(err)

- 

-     def mark_dir_with_build_id(self):

-         """

-             Places "build.info" which contains job build_id

-                 into the directory with downloaded files.

- 

-         """

-         info_file_path = os.path.join(self.job.results_dir, "build.info")

-         self.log.info("marking build dir with build_id, ")

-         try:

-             with open(info_file_path, 'w') as info_file:

-                 info_file.writelines([

-                     "build_id={}".format(self.job.build_id),

-                     "\nbuilder_ip={}".format(self.builder.hostname)])

- 

-         except Exception as error:

-             self.log.exception("Failed to mark build {} with build_id".format(error))

@@ -1,185 +0,0 @@

- import os

- import pipes

- from subprocess import Popen

- 

- from copr_backend.vm_manage import PUBSUB_INTERRUPT_BUILDER

- 

- import gi

- gi.require_version('Modulemd', '1.0')

- from gi.repository import Modulemd

- 

- from ..helpers import get_redis_connection, ensure_dir_exists

- from ..exceptions import BuilderError, RemoteCmdError, VmError

- from ..constants import rsync

- from ..sshcmd import SSHConnection

- 

- 

- 

- class Builder(object):

- 

-     def __init__(self, opts, hostname, job, logger):

- 

-         self.opts = opts

-         self.hostname = hostname

-         self.job = job

-         self.timeout = self.job.timeout or self.opts.timeout

-         self.log = logger

- 

-         # BACKEND/BUILDER API

-         self.builddir = "/var/lib/copr-rpmbuild"

-         self.livelog_name = os.path.join(self.builddir, 'main.log')

- 

-         self.resultdir = os.path.join(self.builddir, 'results')

-         self.pidfile = os.path.join(self.builddir, 'pid')

- 

-         self.conn = SSHConnection(

-             user=self.opts.build_user,

-             host=self.hostname,

-             config_file=self.opts.ssh.builder_config

-         )

- 

-         self.module_dist_tag = None

-         self._build_pid = None

- 

-     def _run_ssh_cmd(self, cmd):

-         """

-         Executes single shell command remotely

- 

-         :param str cmd: shell command

-         :return: stdout, stderr as strings

-         """

-         self.log.info("BUILDER CMD: "+cmd)

-         rc, out, err = self.conn.run_expensive(cmd)

-         if rc != 0:

-             raise RemoteCmdError("Error running remote ssh command.",

-                                  cmd, rc, err, out)

-         return out, err

- 

-     def check_build_success(self):

-         """

-         Check if the build succeeded.  If yes, return True.

-         """

-         successfile = os.path.join(self.resultdir, "success")

-         return self.conn.run("/usr/bin/test -f {0}".format(successfile)) == 0

- 

-     def run_async_build(self):

-         cmd = self._copr_builder_cmd()

-         pid, _ = self._run_ssh_cmd(cmd)

-         self._build_pid = int(pid.strip())

- 

-     def setup_pubsub_handler(self):

-         # TODO: is this used?

-         self.rc = get_redis_connection(self.opts)

-         self.ps = self.rc.pubsub(ignore_subscribe_messages=True)

-         channel_name = PUBSUB_INTERRUPT_BUILDER.format(self.hostname)

-         self.ps.subscribe(channel_name)

- 

-         self.log.info("Subscribed to vm interruptions channel {}".format(channel_name))

- 

-     def check_pubsub(self):

-         # self.log.info("Checking pubsub channel")

-         msg = self.ps.get_message()

-         if msg is not None and msg.get("type") == "message":

-             raise VmError("Build interrupted by msg: {}".format(msg["data"]))

- 

-     @property

-     def build_pid(self):

-         if not self._build_pid:

-             try:

-                 pidof_cmd = "cat {0}".format(self.pidfile)

-                 out, _ = self._run_ssh_cmd(pidof_cmd)

-                 self._build_pid = int(out.strip())

-             except:

-                 return None

- 

-         return self._build_pid

- 

-     def _copr_builder_cmd(self):

-         return 'copr-rpmbuild --verbose --drop-resultdir '\

-                '--build-id {build_id} --chroot {chroot} --detached'.format(

-                    build_id=self.job.build_id, chroot=self.job.chroot)

- 

-     def attach_to_build(self):

-         if not self.build_pid:

-             self.log.info("Build is not running. Continuing...")

-             return

- 

-         ensure_dir_exists(self.job.results_dir, self.log)

-         live_log = os.path.join(self.job.results_dir, 'builder-live.log')

- 

-         live_cmd = '/usr/bin/tail -F -n +0 --pid={pid} {log}'.format(

-             pid=self.build_pid, log=self.livelog_name)

- 

-         self.log.info("Attaching to live build log: " + live_cmd)

-         with open(live_log, 'w') as logfile:

-             # Ignore the exit status.

-             self.conn.run(live_cmd, stdout=logfile, stderr=logfile)

- 

-     def build(self):

-         # run the build

-         self.run_async_build()

- 

-         # attach to building output

-         self.attach_to_build()

- 

-     def reattach(self):

-         self.attach_to_build()

- 

-     def rsync_call(self, source_path, target_path):

-         # TODO: sshcmd.py uses pre-allocated socket, use it here, too

-         ensure_dir_exists(target_path, self.log)

- 

-         # make spaces work w/our rsync command below :(

-         target_path = "'" + target_path.replace("'", "'\\''") + "'"

- 

-         ssh_opts = "'ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no'"

-         full_source_path = "{}@{}:{}/*".format(self.opts.build_user,

-                                                self.hostname,

-                                                source_path)

-         log_filepath = os.path.join(target_path, self.job.rsync_log_name)

-         command = "{} -rlptDvH -e {} {} {}/ &> {}".format(

-             rsync, ssh_opts, full_source_path, target_path, log_filepath)

- 

-         # dirty magic with Popen due to IO buffering

-         # see http://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/

-         # alternative: use tempfile.Tempfile as Popen stdout/stderr

-         try:

-             self.log.info("rsyncing of {0} started for job: {1}".format(full_source_path, self.job))

-             cmd = Popen(command, shell=True)

-             cmd.wait()

-             self.log.info("rsyncing finished.")

-         except Exception as error:

-             err_msg = ("Failed to download data from builder due to rsync error, "

-                        "see the rsync log file for details. Original error: {}".format(error))

-             self.log.error(err_msg)

-             raise BuilderError(err_msg)

- 

-         if cmd.returncode != 0:

-             err_msg = "Failed to download data from builder due to rsync error, see the rsync log file for details."

-             self.log.error(err_msg)

-             raise BuilderError(err_msg)

- 

-     def download_results(self, target_path):

-         self.rsync_call(self.resultdir, target_path)

- 

-     def check(self):

-         try:

-             self._run_ssh_cmd("/bin/rpm -q copr-rpmbuild")

-         except RemoteCmdError:

-             raise BuilderError("Build host `{0}` does not have copr-rpmbuild installed"

-                                .format(self.hostname))

- 

-         # test for path existence for chroot config

-         if self.job.chroot != "srpm-builds":

-             try:

-                 self._run_ssh_cmd("/usr/bin/test -f /etc/mock/{}.cfg"

-                                   .format(self.job.chroot))

-             except RemoteCmdError:

-                 raise BuilderError("Build host `{}` missing mock config for chroot `{}`"

-                                    .format(self.hostname, self.job.chroot))

- 

- 

- class SrpmBuilder(Builder):

-     def _copr_builder_cmd(self):

-         return 'copr-rpmbuild --verbose --drop-resultdir '\

-                '--srpm --build-id {build_id} --detached'.format(build_id=self.job.build_id)

@@ -26,11 +26,6 @@

      StompConnectionListener = object

  

  

- class _LogAdapter(logging.LoggerAdapter):

-     def process(self, msg, kwargs):

-         return "[BUS '{0}'] {1}".format(self.extra['bus_id'], msg), kwargs

- 

- 

  def message_from_worker_job(style, topic, job, who, ip, pid):

      """

      Compat wrapper generating message object for messages defined before we
@@ -140,8 +135,8 @@

      An "abstract" message bus class, don't instantiate!

      """

      messages = {}

- 

      style = 'v1'

+     bus_type = "msgbus"

  

      def __init__(self, opts, log=None):

          self.opts = opts
@@ -150,12 +145,11 @@

  

          self.opts.bus_publish_retries = getattr(self.opts, 'bus_publish_retries', 5)

  

+         self.log = log

          if not log:

-             log = logging

+             self.log = logging

              logging.basicConfig(level=logging.DEBUG)

  

-         self.log = _LogAdapter(log, {'bus_id': self.opts.bus_id})

- 

          self.log.info("initializing bus")

          if hasattr(self.opts, 'messages'):

              self.messages.update(self.opts.messages)
@@ -194,6 +188,11 @@

          msg = message_from_worker_job(self.style, msg_type, job, who, ip, pid)

          self.send_message(msg)

  

+     @property

+     def info(self):

+         """ string info about the bus """

+         return "{} bus".format(self.bus_type)

+ 

  

  class StompListener(StompConnectionListener):

      def __init__(self, msgbus):
@@ -215,6 +214,7 @@

      """

  

      style = 'v1stomp'

+     bus_type = "stomp"

  

      def connect(self):

          """
@@ -301,6 +301,9 @@

      """

      Connect to fedmsg and send messages over it.

      """

+ 

+     bus_type = "fedmsg"

+ 

      def __init__(self, log=None):

          # Hack to not require opts argument for now.

          opts = type('', (), {})
@@ -318,6 +321,9 @@

      """

      Connect to fedora-messaging AMQP bus and send messages over it.

      """

+ 

+     bus_type = "fedora-messaging"

+ 

      def __init__(self, opts, log=None):

          super(MsgBusFedoraMessaging, self).__init__(opts, log)

          # note this is not thread safe, only one bus of this type!
@@ -326,3 +332,36 @@

      def _send_message(self, message):

          from fedora_messaging import api as fm_api, exceptions as fm_ex

          fm_api.publish(message)

+ 

+ 

+ class MessageSender:

+     """

+     Automatically send messages to all configured buses.

+     """

+     def __init__(self, backend_opts, name, log):

+         self.log = log

+         self.name = name

+ 

+         msg_buses = []

+         for bus_config in backend_opts.msg_buses:

+             if bus_config.bus_type == 'stomp':

+                 msg_buses.append(MsgBusStomp(bus_config, log))

+             elif bus_config.bus_type == 'fedora-messaging':

+                 msg_buses.append(MsgBusFedoraMessaging(bus_config, log))

+ 

+         if backend_opts.fedmsg_enabled:

+             msg_buses.append(MsgBusFedmsg(log))

+ 

+         self.msg_buses = msg_buses

+         self.pid = os.getpid()

+ 

+     def announce(self, topic, job, host):

+         """ Send message to all configured buses """

+         for bus in self.msg_buses:

+             self.log.info("Sending %s message in %s", bus.info, topic)

+             bus.announce_job(

+                 topic, job,

+                 who=self.name,

+                 ip=host,

+                 pid=self.pid

+             )

@@ -25,6 +25,11 @@

      def __init__(self, task):

          self._task = task

          self._backend_priority = 0

+         try:

+             int(self.id)

+             self.source_build = True

+         except ValueError:

+             self.source_build = False

  

      @property

      def frontend_priority(self):
@@ -57,10 +62,7 @@

          The chroot this task will be built in.  We return 'source' if this is

          source RPM build - in such case the build should be arch agnostic.

          """

-         task_chroot = self._task.get('chroot')

-         if not task_chroot:

-             task_chroot = 'srpm-builds'

-         return task_chroot

+         return self._task.get('chroot')

  

      @property

      def owner(self):
@@ -73,9 +75,10 @@

          What is the requested "native" builder architecture for which this

          build task will be done.  We use this for limiting the build queue

          (i.e. separate limit for armhfp, even though such build process is

-         emulated on x86_64).

+         emulated on x86_64).  Note that source builds also may require specific

+         chroot (and thus architecture).

          """

-         if self.chroot == "srpm-builds":

+         if not self.chroot:

              return None

          arch = get_chroot_arch(self.chroot)

          if arch.endswith("86"):
@@ -120,7 +123,7 @@

              "copr-backend-process-build",

              "--daemon",

              "--build-id", str(task.build_id),

-             "--chroot", task.chroot,

+             "--chroot", "srpm-builds" if task.source_build else task.chroot,

              "--worker-id", worker_id,

          ]

          self.log.info("running worker: %s", " ".join(command))

file modified
+101 -6
@@ -1,4 +1,6 @@

+ import logging

  import os

+ import time

  import subprocess

  

  class SSHConnectionError(Exception):
@@ -42,10 +44,19 @@

          the default ssh configuration is used /etc/ssh_config and ~/.ssh/config.

      """

  

-     def __init__(self, user=None, host=None, config_file=None):

+     def __init__(self, user=None, host=None, config_file=None, log=None):

+         # TODO: Some of the calling code places heavily re-try the ssh

+         # connection..  There's a some small chance that the host goes down, and

+         # some other host is started with the same hostname (or IP address).

+         # Therefore we should remember the host's SSH fingerprint here and check

+         # it when reconnecting.

          self.config_file = config_file

          self.user = user or 'root'

          self.host = host or 'localhost'

+         if log:

+             self.log = log

+         else:

+             self.log = logging.getLogger()

  

      def _ssh_base(self):

          cmd = ['ssh']
@@ -66,7 +77,7 @@

  

          return retval

  

-     def run(self, user_command, stdout=None, stderr=None):

+     def run(self, user_command, stdout=None, stderr=None, max_retries=0):

          """

          Run user_command (blocking) and redirect stdout and/or stderr into

          pre-opened python file descriptor.  When stdout/stderr is not set, the
@@ -75,6 +86,12 @@

          :param user_command:

              Command (string) to be executed (note: use pipes.quote).

  

+         :param max_retries:

+             When there is ssh connection problem, re-try the action at most

+             ``max_retries`` times.  Default is no re-try.  Note that we write

+             the output from all the re-tries to the stdout/stderr descriptors

+             (when specified).

+ 

          :param stdout:

              File descriptor to write standard output into.

  
@@ -82,8 +99,7 @@

              File descriptor to write standard error output into.

  

          :returns:

-             Triple (rc, stdout, stderr).  Stdout and stderr are of type str,

-             and might be pretty large.

+             Exit status of remote program, or -1 when unexpected failure occurs.

  

          :type command: str

          :type stdout: file or None
@@ -93,10 +109,11 @@

          """

          rc = -1

          with open(os.devnull, "w") as devnull:

-             rc = self._run(user_command, stdout or devnull, stderr or devnull)

+             rc = self._retry(self._run, max_retries,

+                              user_command, stdout or devnull, stderr or devnull)

          return rc

  

-     def run_expensive(self, user_command):

+     def run_expensive(self, user_command, max_retries=0):

          """

          Run user_command (blocking) and return exit status together with

          standard outputs in string variables.  Note that this can pretty easily
@@ -105,10 +122,17 @@

          :param user_command:

              Command (string) to be run as string (note: use pipes.quote).

  

+         :param max_retries:

+             When there is ssh connection problem, re-try the action at most

+             ``max_retries`` times.  Default is no re-try.

+ 

          :returns:

              Tripple (rc, stdout, stderr).  Stdout and stderr are strings, those

              might be pretty large.

          """

+         return self._retry(self._run_expensive, max_retries, user_command)

+ 

+     def _run_expensive(self, user_command):

          real_command = self._ssh_base() + [user_command]

          proc = subprocess.Popen(real_command, stdout=subprocess.PIPE,

                                  stderr=subprocess.PIPE, encoding="utf-8")
@@ -123,3 +147,74 @@

                      stdout, stderr))

  

          return proc.returncode, stdout, stderr

+ 

+     def _retry(self, method, retries, *args, **kwargs):

+         """ Do ``times`` when SSHConnectionError is raised """

+         attempt = 0

+         while attempt < retries + 1:

+             attempt += 1

+             try:

+                 return method(*args, **kwargs)

+             except SSHConnectionError as exc:

+                 sleep = 10

+                 self.log.error("SSH connection lost on #%s attempt, "

+                                "let's retry after %ss, %s", attempt, sleep, exc)

+                 time.sleep(sleep)

+                 continue

+         raise SSHConnectionError("Unable to finish after {} SSH attempts"

+                                  .format(attempt))

+ 

+     def _full_source_path(self, src):

+         """ for easier unittesting """

+         return "{}@{}:{}".format(self.user, self.host, src)

+ 

+     def rsync_download(self, src, dest, logfile=None, max_retries=0):

+         """

+         Run rsync over pre-allocated socket (by the config)

+ 

+         :param src:

+             Source path on self.host to copy.

+ 

+         :param dest:

+             Destination path on backend to copy ``src` content to.

+ 

+         :param max_retries:

+             When there is ssh connection problem, re-try the action at most

+             ``max_retries`` times.  Default is no re-try.

+ 

+         Store the logs to ``logfile`` within ``dest`` directory.  The dest

+         directory needs to exist.

+         """

+         self._retry(self._rsync_download, max_retries, src, dest, logfile)

+ 

+     def _rsync_download(self, src, dest, logfile=None):

+         ssh_opts = "ssh"

+         if self.config_file:

+             ssh_opts += " -F " + self.config_file

+ 

+         full_source_path = self._full_source_path(src)

+ 

+         log_filepath = "/dev/null"

+         if logfile:

+             log_filepath = os.path.join(dest, logfile)

+         command = "/usr/bin/rsync -rlptDvH -e '{}' {} {}/ &> {}".format(

+             ssh_opts, full_source_path, dest, log_filepath)

+ 

+         try:

+             self.log.info("rsyncing of %s to %s started", full_source_path, dest)

+             cmd = subprocess.Popen(command, shell=True)

+             cmd.wait()

+             self.log.info("rsyncing finished.")

+         except Exception as error:

+             self.log.error(

+                 "Failed to download data from builder due to Popen error, "

+                 "original error: %s", error)

+             raise SSHConnectionError("POpen failure in rsync.")

+ 

+         if cmd.returncode != 0:

+             err_msg = (

+                 "Failed to download data from builder due to rsync error, "

+                 "see the rsync log file for details."

+             )

+             self.log.error(err_msg)

+             raise SSHConnectionError(err_msg)

@@ -1,39 +0,0 @@

- # coding: utf-8

- 

- 

- class VmStates(object):

-     GOT_IP = "got_ip"

-     CHECK_HEALTH = "check_health"

-     CHECK_HEALTH_FAILED = "check_health_failed"

-     READY = "ready"

-     IN_USE = "in_use"

-     TERMINATING = "terminating"

- 

- # for IPC

- PUBSUB_MB = "copr:backend:vm:pubsub::"

- 

- 

- class EventTopics(object):

-     HEALTH_CHECK = "health_check"

-     VM_SPAWNED = "vm_spawned"

-     VM_TERMINATION_REQUEST = "vm_termination_request"

-     VM_TERMINATED = "vm_terminated"

- 

- # argument - vm_ip

- PUBSUB_INTERRUPT_BUILDER = "copr:backend:interrupt_build:pubsub::{}"

- 

- 

- KEY_VM_POOL = "copr:backend:vm_pool:set::{group}"

- # set of vm_names of vm available for `group`

- 

- KEY_VM_POOL_INFO = "copr:backend:vm_pool_info:hset::{group}"

- # hset with additional information for `group`, used fields:

- # - "last_vm_spawn_start": latest time when VM spawn was initiated for this `group`

- 

- KEY_SERVER_INFO = "copr:backend:server_info:hset::"

- # common shared info about server, not stritly related to VMM, maybe move it to helpers later

- # used fields:

- #   "server_start_timestamp" -> unixtime string

- 

- KEY_VM_INSTANCE = "copr:backend:vm_instance:hset::{vm_name}"

- # hset to store VmDescriptor

@@ -1,65 +0,0 @@

- # coding: utf-8

- import json

- #from setproctitle import setproctitle

- # from multiprocessing import Process

- #from threading import Thread

- 

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage import PUBSUB_MB, EventTopics

- from copr_backend.vm_manage.executor import Executor

- 

- from ..helpers import get_redis_logger

- from ..sshcmd import SSHConnection

- 

- 

- def check_health(opts, vm_name, vm_ip):

-     """

-     Test connectivity to the VM

- 

-     :param vm_ip: ip address to the newly created VM

-     :raises: :py:class:`~copr_backend.exceptions.CoprWorkerSpawnFailError`: validation fails

-     """

-     log = get_redis_logger(opts, "vmm.check_health.detached", "vmm")

- 

-     result = {

-         "vm_ip": vm_ip,

-         "vm_name": vm_name,

-         "msg": "",

-         "result": "OK",

-         "topic": EventTopics.HEALTH_CHECK

-     }

- 

-     err_msg = None

-     try:

-         conn = SSHConnection(opts.build_user or "root", vm_ip, config_file=opts.ssh.builder_config)

-         rc, stdout, _ = conn.run_expensive("echo hello")

-         if rc != 0 or stdout != "hello\n":

-             err_msg = "Unexpected check output"

-     except Exception as error:

-         err_msg = "Healtcheck failed for VM {} with error {}".format(vm_ip, error)

-         log.exception(err_msg)

- 

-     try:

-         if err_msg:

-             result["result"] = "failed"

-             result["msg"] = err_msg

-         rc = get_redis_connection(opts)

-         rc.publish(PUBSUB_MB, json.dumps(result))

-     except Exception as err:

-         log.exception("Failed to publish msg health check result: %s with error: %s",

-                       result, err)

- 

- 

- class HealthChecker(Executor):

- 

-     __name_for_log__ = "health_checker"

-     __who_for_log__ = "vmm"

- 

-     def run_check_health(self, vm_name, vm_ip):

-         self.recycle()

-         self.run_detached(check_health, args=(self.opts, vm_name, vm_ip))

-         # proc = Process(target=check_health, args=(self.opts, vm_name, vm_ip))

-         # proc = Thread(target=check_health, args=(self.opts, vm_name, vm_ip))

-         # self.child_processes.append(proc)

-         # proc.start()

-         # self.log.debug("Check health process started: {}".format(proc.pid))

@@ -1,176 +0,0 @@

- # coding: utf-8

- import json

- from multiprocessing import Process

- from threading import Thread

- import time

- from setproctitle import setproctitle

- 

- from copr_backend.exceptions import VmDescriptorNotFound

- from copr_backend.helpers import get_redis_logger

- from copr_backend.vm_manage import VmStates, PUBSUB_MB, EventTopics

- 

- 

- class Recycle(Thread):

-     """

-     Cleanup vmm services, now only terminator

-     :param vmm:

-     :return:

-     """

-     def __init__(self, terminator, recycle_period, *args, **kwargs):

-         self.terminator = terminator

-         self.recycle_period = int(recycle_period)

-         super(Recycle, self).__init__(*args, **kwargs)

-         self._running = False

- 

-     def run(self):

- 

-         self._running = True

-         while self._running:

-             time.sleep(self.recycle_period)

-             self.terminator.recycle()

- 

-     def terminate(self):

-         self._running = False

- 

- # KEYS[1]: VMD key

- on_health_check_success_lua = """

- local old_state = redis.call("HGET", KEYS[1], "state")

- if old_state ~= "check_health" and old_state ~= "in_use" then

-     return nil

- else

-     redis.call("HSET", KEYS[1], "check_fails", 0)

-     if old_state == "check_health" then

-         redis.call("HSET", KEYS[1], "state", "{}")

-     end

- end

- """.format(VmStates.READY)

- 

- # KEYS[1]: VMD key

- record_failure_lua = """

- local old_state = redis.call("HGET", KEYS[1], "state")

- if old_state ~= "check_health" and old_state ~= "in_use" and old_state ~= "check_health_failed" then

-     return nil

- else

-     redis.call("HINCRBY", KEYS[1], "check_fails", 1)

-     if old_state == "check_health" then

-         redis.call("HSET", KEYS[1], "state", "{}")

-     end

- end

- """.format(VmStates.CHECK_HEALTH_FAILED)

- 

- 

- class EventHandler(Process):

-     """

-     :type vmm: VmManager

-     """

-     def __init__(self, opts, vmm, terminator):

-         super(EventHandler, self).__init__(name="EventHandler")

-         self.opts = opts

-         self.vmm = vmm

- 

-         self.terminator = terminator

- 

-         self.kill_received = False

- 

-         # self.do_recycle_proc = None

-         self.handlers_map = {

-             EventTopics.HEALTH_CHECK: self.on_health_check_result,

-             EventTopics.VM_SPAWNED: self.on_vm_spawned,

-             EventTopics.VM_TERMINATION_REQUEST: self.on_vm_termination_request,

-             EventTopics.VM_TERMINATED: self.on_vm_termination_result,

-         }

-         self.lua_scripts = {}

-         self.recycle_period = 60

- 

-         self.log = get_redis_logger(self.vmm.opts, "vmm.event_handler", "vmm")

-         self.vmm.set_logger(self.log)

- 

-     def post_init(self):

-         # todo: move to manager.py, wrap call into VmManager methods

-         self.lua_scripts["on_health_check_success"] = self.vmm.rc.register_script(on_health_check_success_lua)

-         self.lua_scripts["record_failure"] = self.vmm.rc.register_script(record_failure_lua)

- 

-     def on_health_check_result(self, msg):

-         try:

-             vmd = self.vmm.get_vm_by_name(msg["vm_name"])

-             check_fails_count = int(vmd.get_field(self.vmm.rc, "check_fails") or 0)

-         except VmDescriptorNotFound:

-             self.log.debug("VM record disappeared, ignoring health check results,  msg: %s", msg)

-             return

- 

-         if msg["result"] == "OK":

-             self.lua_scripts["on_health_check_success"](keys=[vmd.vm_key], args=[time.time()])

-             self.log.debug("recording success for ip:%s name:%s", vmd.vm_ip, vmd.vm_name)

-         else:

-             self.log.debug("recording check fail: %s", msg)

-             self.lua_scripts["record_failure"](keys=[vmd.vm_key])

-             fails_count = int(vmd.get_field(self.vmm.rc, "check_fails") or 0)

-             max_check_fails = self.opts.build_groups[vmd.group]["vm_max_check_fails"]

-             if fails_count > max_check_fails:

-                 self.log.info("check fail threshold reached: %s, terminating: %s",

-                               check_fails_count, msg)

-                 self.vmm.start_vm_termination(vmd.vm_name)

- 

-     def on_vm_spawned(self, msg):

-         self.vmm.add_vm_to_pool(vm_ip=msg["vm_ip"], vm_name=msg["vm_name"], group=msg["group"])

- 

-     def on_vm_termination_request(self, msg):

-         self.terminator.terminate_vm(vm_ip=msg["vm_ip"], vm_name=msg["vm_name"], group=msg["group"])

- 

-     def on_vm_termination_result(self, msg):

-         if msg["result"] == "OK" and "vm_name" in msg:

-             self.log.debug("Vm terminated, removing from pool ip: %s, name: %s, msg: %s",

-                            msg.get("vm_ip"), msg.get("vm_name"), msg.get("msg"))

-             self.vmm.remove_vm_from_pool(msg["vm_name"])

-         elif "vm_name" not in msg:

-             self.log.debug("Vm termination event missing vm name, msg: %s", msg)

-         else:

-             self.log.debug("Vm termination failed ip: %s, name: %s, msg: %s",

-                            msg.get("vm_ip"), msg.get("vm_name"), msg.get("msg"))

- 

-     def run(self):

-         setproctitle("Event Handler")

- 

-         self.do_recycle_proc = Recycle(terminator=self.terminator, recycle_period=self.recycle_period)

-         self.do_recycle_proc.start()

- 

-         self.start_listen()

- 

-     def terminate(self):

-         self.kill_received = True

-         self.do_recycle_proc.terminate()

-         self.do_recycle_proc.join()

- 

-     def start_listen(self):

-         """

-         Listens redis pubsub and perform requested actions.

-         Message payload is packed in json, it should be a dictionary

-             at the root level with reserved field `topic` which is required

-             for message routing

-         :type vmm: VmManager

-         """

-         channel = self.vmm.rc.pubsub(ignore_subscribe_messages=True)

-         channel.subscribe(PUBSUB_MB)

-         # TODO: check subscribe success

-         self.log.info("Spawned pubsub handler")

-         for raw in channel.listen():

-             if self.kill_received:

-                 break

-             if raw is None:

-                 continue

-             else:

-                 if "type" not in raw or raw["type"] != "message" or "data" not in raw:

-                     continue

-                 try:

-                     msg = json.loads(raw["data"])

- 

-                     if "topic" not in msg:

-                         raise Exception("Handler received msg without `topic` field, msg: {}".format(msg))

-                     topic = msg["topic"]

-                     if topic not in self.handlers_map:

-                         raise Exception("Handler received msg with unknown `topic` field, msg: {}".format(msg))

- 

-                     self.handlers_map[topic](msg)

- 

-                 except Exception as err:

-                     self.log.exception("Handler error: raw msg: %s, %s", raw, err)

@@ -1,74 +0,0 @@

- # coding: utf-8

- import threading

- import time

- from ..helpers import get_redis_logger

- 

- 

- class Executor(object):

-     """

-     Helper super-class to run background processes and clean up after them.

- 

-     Child class should have method which spawns subprocess and add it handler to `self.child_processes` list.

-     Also don't forget to call recycle

-     """

-     __name_for_log__ = "executor"

-     __who_for_log__ = "executor"

- 

-     def __init__(self, opts):

-         self.opts = opts

- 

-         self.child_processes = []

-         self.last_recycle = time.time()

-         self.recycle_period = 60

- 

-         self.log = get_redis_logger(self.opts, "vmm.{}".format(self.__name_for_log__), self.__who_for_log__)

- 

-     def run_detached(self, func, args):

-         """

-         Abstaction to spawn Thread or Process

-         :return:

-         """

-         # proc = Process(target=func, args=(args))

-         proc = threading.Thread(target=func, args=args)

-         self.child_processes.append(proc)

-         proc.start()

-         # self.log.debug("Spawn process started: {}".format(proc.pid))

-         return proc

- 

-     def after_proc_finished(self, proc):

-         # hook for subclasses

-         pass

- 

-     def recycle(self, force=False):

-         """

-         Cleanup unused process, should be invoked periodically

-         :param force: do recycle now unconditionally

-         :type force: bool

-         """

-         if not force and time.time() - self.last_recycle < self.recycle_period:

-             return

- 

-         self.last_recycle = time.time()

- 

-         self.log.debug("Running recycle {}")

-         still_alive = []

-         for proc in self.child_processes:

-             if proc.is_alive():

-                 still_alive.append(proc)

-             else:

-                 proc.join()

-                 # self.log.debug("Child process finished: {}".format(proc.pid))

-                 self.log.debug("Child process finished: %s", proc)

-                 self.after_proc_finished(proc)

- 

-         self.child_processes = still_alive

- 

-     def terminate(self):

-         for proc in self.child_processes:

-             proc.terminate()

-             proc.join()

- 

-     @property

-     def children_number(self):

-         self.recycle()

-         return len(self.child_processes)

@@ -1,458 +0,0 @@

- # coding: utf-8

- 

- import json

- import time

- import weakref

- import datetime

- import tabulate

- import humanize

- from io import StringIO

- from copr_backend.exceptions import VmError, NoVmAvailable, VmDescriptorNotFound

- 

- from copr_backend.helpers import get_redis_connection

- from .models import VmDescriptor

- from . import VmStates, KEY_VM_INSTANCE, KEY_VM_POOL, EventTopics, PUBSUB_MB, KEY_SERVER_INFO, \

-     KEY_VM_POOL_INFO

- from ..helpers import get_redis_logger

- 

- # KEYS[1]: VMD key

- # ARGV[1] current timestamp for `last_health_check`

- set_checking_state_lua = """

- local old_state = redis.call("HGET", KEYS[1], "state")

- if old_state ~= "got_ip" and old_state ~= "ready" and old_state ~= "in_use" and old_state ~= "check_health_failed" then

-     return nil

- else

-     if old_state ~= "in_use" then

-         redis.call("HSET", KEYS[1], "state", "check_health")

-     end

-     redis.call("HSET", KEYS[1], "last_health_check", ARGV[1])

-     return "OK"

- end

- """

- 

- # KEYS[1]: VMD key

- # ARGV[1]: user to bound;

- # ARGV[2]: pid of the builder process

- # ARGV[3]: current timestamp for `in_use_since`

- # ARGV[4]: task_id

- # ARGV[5]: build_id

- # ARGV[6]: chroot

- # ARGV[7]: sandbox

- acquire_vm_lua = """

- local old_state = redis.call("HGET", KEYS[1], "state")

- if old_state ~= "ready"  then

-     return nil

- else

-     local last_health_check = tonumber(redis.call("HGET", KEYS[1], "last_health_check"))

-     local server_restart_time = tonumber(redis.call("HGET", KEYS[2], "server_start_timestamp"))

-     if last_health_check and server_restart_time and last_health_check > server_restart_time  then

-         redis.call("HMSET", KEYS[1], "state", "in_use", "bound_to_user", ARGV[1],

-                    "used_by_worker", ARGV[2], "in_use_since", ARGV[3],

-                    "task_id",  ARGV[4], "build_id", ARGV[5], "chroot", ARGV[6],

-                    "sandbox", ARGV[7])

-         return "OK"

-     else

-         return nil

-     end

- end

- """

- 

- # KEYS[1]: VMD key

- # ARGV[1] current timestamp for `last_release`

- release_vm_lua = """

- local old_state = redis.call("HGET", KEYS[1], "state")

- if old_state ~= "in_use" then

-     return nil

- else

-     redis.call("HMSET", KEYS[1], "state", "ready", "last_release", ARGV[1])

-     redis.call("HDEL", KEYS[1], "in_use_since", "used_by_worker", "task_id", "build_id", "chroot")

-     redis.call("HINCRBY", KEYS[1], "builds_count", 1)

- 

-     local check_fails = tonumber(redis.call("HGET", KEYS[1], "check_fails"))

-     if check_fails and check_fails > 0 then

-         redis.call("HSET", KEYS[1], "state", "check_health_failed")

-     end

- 

-     return "OK"

- end

- """

- 

- # KEYS [1]: VMD key

- # ARGS [1]: allowed_pre_state

- # ARGS [2]: timestamp for `terminating_since`

- terminate_vm_lua = """

- local old_state = redis.call("HGET", KEYS[1], "state")

- 

- if ARGV[1] and ARGV[1] ~= "None" and old_state ~= ARGV[1] then

-     return "Old state != `allowed_pre_state`"

- elseif old_state == "terminating" and ARGV[1] ~= "terminating" then

-     return "Already terminating"

- else

-     redis.call("HMSET", KEYS[1], "state", "terminating", "terminating_since", ARGV[2])

-     return "OK"

- end

- """

- 

- mark_vm_check_failed_lua = """

- local old_state = redis.call("HGET", KEYS[1], "state")

- if old_state == "check_health" then

-     redis.call("HMSET", KEYS[1], "state", "check_health_failed")

-     return "OK"

- end

- """

- 

- 

- class VmManager(object):

-     """

-     VM manager, it is used for two purposes:

-     - Daemon which control VMs lifecycle, requires params `spawner,terminator`

-     - Client to acquire and release VM in builder process

- 

-     :param opts: Global backend configuration

-     :type opts: Munch

- 

-     :type logger: logging.Logger

-     :param logger: Logger instance to use inside of manager, if None manager would create

-         new logger using helpers.get_redis_logger

-     """

-     def __init__(self, opts, logger=None):

- 

-         self.opts = weakref.proxy(opts)

- 

-         self.lua_scripts = {}

- 

-         self.rc = None

-         self.log = logger or get_redis_logger(self.opts, "vmm.lib", "vmm")

- 

-         self.rc = get_redis_connection(self.opts)

-         self.lua_scripts["set_checking_state"] = self.rc.register_script(set_checking_state_lua)

-         self.lua_scripts["acquire_vm"] = self.rc.register_script(acquire_vm_lua)

-         self.lua_scripts["release_vm"] = self.rc.register_script(release_vm_lua)

-         self.lua_scripts["terminate_vm"] = self.rc.register_script(terminate_vm_lua)

-         self.lua_scripts["mark_vm_check_failed"] = self.rc.register_script(mark_vm_check_failed_lua)

- 

-     def set_logger(self, logger):

-         """

-         :param logger: Logger to be used by manager

-         :type logger: logging.Logger

-         """

-         self.log = logger

- 

-     @property

-     def vm_groups(self):

-         """

-         :return: VM build groups

-         :rtype: list of int

-         """

-         return range(self.opts.build_groups_count)

- 

-     def add_vm_to_pool(self, vm_ip, vm_name, group):

-         """

-         Adds newly spawned VM into the pool of available builders

- 

-         :param str vm_ip: IP

-         :param str vm_name: VM name

-         :param group: builder group

-         :type group: int

-         :rtype: VmDescriptor

-         """

-         if self.rc.sismember(KEY_VM_POOL.format(group=group), vm_name):

-             raise VmError("Can't add VM `{}` to the pool, such name already used".format(vm_name))

- 

-         vmd = VmDescriptor(vm_ip, vm_name, group, VmStates.GOT_IP)

-         pipe = self.rc.pipeline()

-         pipe.sadd(KEY_VM_POOL.format(group=group), vm_name)

-         pipe.hmset(KEY_VM_INSTANCE.format(vm_name=vm_name), vmd.to_dict())

-         pipe.execute()

-         self.log.info("registered new VM: %s %s", vmd.vm_name, vmd.vm_ip)

- 

-         return vmd

- 

-     def lookup_vms_by_ip(self, vm_ip):

-         """

-         :param vm_ip:

-         :return: List of found VMD with the give ip

-         :rtype: list of VmDescriptor

-         """

-         return [

-             vmd for vmd in self.get_all_vm()

-             if vmd.vm_ip == vm_ip

-         ]

- 

-     def mark_vm_check_failed(self, vm_name):

-         vm_key = KEY_VM_INSTANCE.format(vm_name=vm_name)

-         self.lua_scripts["mark_vm_check_failed"](keys=[vm_key])

- 

-     def mark_server_start(self):

-         self.rc.hset(KEY_SERVER_INFO, "server_start_timestamp", time.time())

- 

-     def can_user_acquire_more_vm(self, username, group):

-         """

-         :return bool: True when user are allowed to acquire more VM

-         """

-         vmd_list = self.get_all_vm_in_group(group)

-         vm_count_used_by_user = len([

-             vmd for vmd in vmd_list if vmd.bound_to_user == username

-         ])

- 

-         limit = self.opts.build_groups[group].get("max_vm_per_user")

-         self.log.debug("# vm by user: %s, limit:%s ",

-                        vm_count_used_by_user, limit)

- 

-         if limit and vm_count_used_by_user >= limit:

-             self.log.debug("No VM are available, user `%s` already acquired #%s VMs",

-                            username, vm_count_used_by_user)

-             return False

-         return True

- 

-     def get_ready_vms(self, group):

-         vmd_list = self.get_all_vm_in_group(group)

-         return [vmd for vmd in vmd_list if vmd.state == VmStates.READY]

- 

-     def acquire_vm(self, groups, ownername, sandbox, pid, task_id="None", build_id="None", chroot="None"):

-         """

-         Try to acquire VM from pool.

- 

-         :param list groups: builder group ids where the build can be launched as defined in config

-         :param ownername: the owner name (user or group) this build is going

-                 to be accounted to, at this moment there's max limit for

-                 concurrent jobs accounted to a single owner

-         :param sandbox: sandbox ID required by the build; we prefer to reuse

-                 existing VMs previously used for the same sandbox

-         :param pid: worker process id

- 

-         :rtype: VmDescriptor

-         :raises: NoVmAvailable when manager couldn't find suitable VM for the given groups and owner

-         """

-         for group in groups:

-             ready_vmd_list = self.get_ready_vms(group)

-             # trying to find VM used by the same owner for the same sandbox

-             dirtied = [vmd for vmd in ready_vmd_list

-                        if vmd.sandbox == sandbox and

-                           vmd.bound_to_user == ownername]

- 

-             user_can_acquire_more_vm = self.can_user_acquire_more_vm(ownername, group)

-             if not dirtied and not user_can_acquire_more_vm:

-                 self.log.debug("User %s already acquired too much VMs in group %s",

-                                ownername, group)

-                 continue

- 

-             available_vms = dirtied

-             if user_can_acquire_more_vm:

-                 clean_list = [vmd for vmd in ready_vmd_list if vmd.bound_to_user is None]

-                 available_vms += clean_list

- 

-             for vmd in available_vms:

-                 check_fails = vmd.get_field(self.rc, "check_fails")

-                 if check_fails and check_fails != "0":

-                     self.log.debug("VM %s has check fails, skip acquire", vmd.vm_name)

-                     continue

- 

-                 vm_key = KEY_VM_INSTANCE.format(vm_name=vmd.vm_name)

-                 if self.lua_scripts["acquire_vm"](keys=[vm_key, KEY_SERVER_INFO],

-                                                   args=[ownername, pid, time.time(),

-                                                         task_id, build_id,

-                                                         chroot, sandbox]) == "OK":

-                     self.log.info("Acquired VM :%s %s for pid: %s", vmd.vm_name, vmd.vm_ip, pid)

-                     return vmd

- 

-         raise NoVmAvailable("No VMs are currently available for task {} of user {}"

-                             .format(task_id, ownername))

- 

-     def release_vm(self, vm_name):

-         """

-         Return VM into the pool.

-         :return: True if successful

-         :rtype: bool

-         """

-         # in_use -> ready

-         self.log.info("Releasing VM %s", vm_name)

-         vm_key = KEY_VM_INSTANCE.format(vm_name=vm_name)

-         lua_result = self.lua_scripts["release_vm"](keys=[vm_key], args=[time.time()])

-         self.log.info("Release vm result `%s`", lua_result)

-         return lua_result == "OK"

- 

-     def start_vm_termination(self, vm_name, allowed_pre_state="None"):

-         """

-         Initiate VM termination process using redis publish.

- 

-         :param allowed_pre_state: When defined force check that old state is among allowed ones.

-         :type allowed_pre_state: str constant from VmState

-         """

-         vmd = self.get_vm_by_name(vm_name)

-         lua_result = self.lua_scripts["terminate_vm"](keys=[vmd.vm_key], args=[allowed_pre_state, time.time()])

-         if lua_result == "OK":

-             msg = {

-                 "group": vmd.group,

-                 "vm_ip": vmd.vm_ip,

-                 "vm_name": vmd.vm_name,

-                 "topic": EventTopics.VM_TERMINATION_REQUEST

-             }

-             self.rc.publish(PUBSUB_MB, json.dumps(msg))

-             self.log.info("VM %s queued for termination", vmd.vm_name)

-         else:

-             self.log.info("VM termination `%s` skipped due to: %s ", vm_name, lua_result)

- 

-     def remove_vm_from_pool(self, vm_name):

-         """

-         Backend forgets about VM after this method

- 

-         :raises VmError: if VM has wrong state

-         """

-         vmd = self.get_vm_by_name(vm_name)

-         if vmd.get_field(self.rc, "state") != VmStates.TERMINATING:

-             raise VmError("VM should have `terminating` state to be removable")

-         pipe = self.rc.pipeline()

-         pipe.srem(KEY_VM_POOL.format(group=vmd.group), vm_name)

-         pipe.delete(KEY_VM_INSTANCE.format(vm_name=vm_name))

-         pipe.execute()

-         self.log.info("removed vm `%s` from pool", vm_name)

- 

-     def _load_multi_safe(self, vm_name_list):

-         result = []

-         for vm_name in vm_name_list:

-             try:

-                 result.append(VmDescriptor.load(self.rc, vm_name))

-             except VmDescriptorNotFound:

-                 self.log.debug("Failed to load VMD: %s", vm_name)

-         return result

- 

-     def get_all_vm_in_group(self, group):

-         """

-         :rtype: list of VmDescriptor

-         """

-         vm_name_list = self.rc.smembers(KEY_VM_POOL.format(group=group))

-         return self._load_multi_safe(vm_name_list)

- 

-     def get_all_vm(self):

-         """

-         :rtype: list of VmDescriptor

-         """

-         vmd_list = []

-         for group in self.vm_groups:

-             if group is None:

-                 continue

-             vmd_list.extend(self.get_all_vm_in_group(group))

-         return vmd_list

- 

-     def get_vm_by_name(self, vm_name):

-         """

-         :rtype: VmDescriptor

-         """

-         return VmDescriptor.load(self.rc, vm_name)

- 

-     def get_vm_by_task_id(self, task_id):

-         """

-         :rtype: VmDescriptor

-         """

-         vmd_list = self.get_all_vm()

-         for vmd in vmd_list:

-             if getattr(vmd, "task_id", None) == task_id:

-                 return vmd

-         return None

- 

-     def get_vm_by_group_and_state_list(self, group, state_list):

-         """

-         Select VM-s for the given group and allowed states

- 

-         :param group: filter VM-s by the build group. If ``group is None`` select VM-s from the all groups

-         :param state_list: VM state should be in the ``state_list``.

- 

-         :return: Filtered VM-s

-         :rtype: list of VmDescriptor

-         """

-         states = set(state_list)

-         if group is None:

-             vmd_list = self.get_all_vm()

-         else:

-             vmd_list = self.get_all_vm_in_group(group)

-         return [vmd for vmd in vmd_list if vmd.state in states]

- 

-     def info(self):

-         """

-         Present information about all managed VMs in a human readable form.

- 

-         :rtype: str

-         """

-         dt_fields = {

-             "last_health_check",

-             "in_use_since",

-         }

- 

-         headers = ['VM Name', 'IP', 'State', 'Health Check', 'Bound to', 'Task info']

- 

-         def date_to_str(value):

-             if value is None:

-                 return 'none'

-             dt = datetime.datetime.fromtimestamp(float(value))

-             return humanize.naturaltime(dt)

- 

-         buf = StringIO()

-         header_spacing = ""

-         for group_id in self.vm_groups:

-             buf.write(header_spacing)

-             header_spacing = "\n\n"

-             bg = self.opts.build_groups[group_id]

-             header = "VM group #{} {} archs: {}".format(group_id, bg["name"], bg["archs"])

-             header_len = len(header)

-             buf.write("=" * header_len + "\n")

-             buf.write(header + "\n")

-             buf.write("=" * header_len + "\n\n")

- 

-             vmd_list = self.get_all_vm_in_group(group_id)

-             rows = []

-             for vm in vmd_list:

-                 row = []

-                 row.append(vm.vm_name)

-                 row.append(vm.vm_ip)

-                 row.append(vm.state)

-                 vmd = vm.to_dict()

- 

-                 row.append("fails: {0}\nlast: {1}".format(

-                     vmd.get('check_fails', 0),

-                     date_to_str(vmd.get('last_health_check', None))

-                 ))

- 

-                 bound_lines = []

-                 user = vmd.get('bound_to_user', '')

-                 sandbox = vmd.get('sandbox', '')

-                 builds_count = vmd.get('builds_count', '')

-                 if user:

-                     bound_lines.append('user: {0}'.format(user))

-                 if sandbox:

-                     bound_lines.append('sandbox: {0}'.format(sandbox))

-                 if builds_count:

-                     bound_lines.append('builds_count: {0}'.format(builds_count))

- 

-                 row.append('\n'.join(bound_lines))

- 

-                 task_info = ''

-                 if vmd.get('task_id', None):

-                     task_info = (

-                         "build: {build_id}\n"

-                         "task: {task_id}\n"

-                         "chroot: {chroot}\n"

-                         "worker: {worker}\n"

-                         "since: {since}"

-                     )

-                     task_info = task_info.format(

-                         build_id=vmd.get('build_id', None),

-                         task_id=vmd.get('task_id', None),

-                         chroot=vmd.get('chroot', None),

-                         since=date_to_str(vmd.get('in_use_since', None)),

-                         worker=vmd.get('used_by_worker', 'unused'),

-                     )

- 

-                 row.append(task_info)

-                 rows.append(row)

- 

-             # sort by the first column

-             buf.write(tabulate.tabulate(sorted(rows, key=(lambda x: x[0])), headers, tablefmt='grid'))

- 

-         return buf.getvalue()

- 

-     def write_vm_pool_info(self, group, key, value):

-         self.rc.hset(KEY_VM_POOL_INFO.format(group=group), key, value)

- 

-     def read_vm_pool_info(self, group, key):

-         return self.rc.hget(KEY_VM_POOL_INFO.format(group=group), key)

@@ -1,76 +0,0 @@

- # coding: utf-8

- 

- from pprint import pformat

- from copr_backend.exceptions import VmDescriptorNotFound

- from . import KEY_VM_INSTANCE

- 

- 

- class VmDescriptor(object):

-     def __init__(self, vm_ip, vm_name, group, state):

-         self.vm_ip = vm_ip

-         self.vm_name = vm_name

-         self.state = state

-         self.group = int(group)

- 

-         self.bound_to_user = None

-         self.used_by_worker = None

-         self.task_id = None

-         self.sandbox = None

- 

-     @property

-     def vm_key(self):

-         return KEY_VM_INSTANCE.format(vm_name=self.vm_name)

- 

-     def __str__(self):

-         return pformat(self.__dict__)

- 

-     def to_dict(self):

-         return {str(k): str(v) for k, v in self.__dict__.items() if v is not None}

- 

-     @classmethod

-     def from_dict(cls, raw):

-         vmd = cls(raw.pop("vm_ip"), raw.pop("vm_name"), raw.pop("group"), raw.pop("state"))

-         vmd.__dict__.update(raw)

-         return vmd

- 

-     @classmethod

-     def load(cls, rc, vm_name):

-         """

- 

-         :param rc:

-         :param vm_name:

-         :rtype: VmDescriptor

-         :raises VmDescriptorNotFound:

-         """

-         raw = rc.hgetall(KEY_VM_INSTANCE.format(vm_name=vm_name))

-         if not raw:

-             raise VmDescriptorNotFound("VmDescriptor for `{}` not found".format(vm_name))

-         return cls.from_dict(raw)

- 

-     def store(self, rc):

-         """

-         :type rc: StrictRedis

-         """

-         rc.hmset(KEY_VM_INSTANCE.format(vm_name=self.vm_name), self.__dict__)

- 

-     def store_field(self, rc, field, value):

-         """

-         :type rc: StrictRedis

-         """

-         # TODO: add option `save_with_existnse_check`, use lua script to ensure that VMD still exists

-         setattr(self, field, value)

-         rc.hset(KEY_VM_INSTANCE.format(vm_name=self.vm_name), field, value)

- 

-     def get_field(self, rc, field):

-         """

-         :type rc: StrictRedis

-         """

-         value = rc.hget(KEY_VM_INSTANCE.format(vm_name=self.vm_name), field)

-         setattr(self, field, value)

-         return value

- 

-     # def record_failure(self, rc):

-     #     """

-     #     :type rc: StrictRedis

-     #     """

-     #     rc.hincrby(KEY_VM_INSTANCE.format(vm_name=self.vm_name), "check_fails")

@@ -1,142 +0,0 @@

- # coding: utf-8

- 

- import json

- import os

- import re

- import time

- 

- from netaddr import IPAddress

- 

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage import PUBSUB_MB, EventTopics

- from copr_backend.vm_manage.executor import Executor

- from ..ans_utils import run_ansible_playbook_cli

- from ..exceptions import CoprSpawnFailError

- from ..helpers import get_redis_logger

- from ..vm_manage import terminate

- 

- def get_ip_from_log(ansible_output):

-     """ Parse IP address from ansible log """

-     match = re.search(r'IP=([^\{\}"\n\\]+)', ansible_output, re.MULTILINE)

-     if not match:

-         raise CoprSpawnFailError("No ip in the result, trying again")

-     return match.group(1)

- 

- def get_vm_name_from_log(ansible_output):

-     """ Parse vm_name from ansible log """

-     match = re.search(r'vm_name=([^\{\}"\n\\]+)', ansible_output, re.MULTILINE)

-     if not match:

-         raise CoprSpawnFailError("No vm_name in the playbook output")

-     return match.group(1)

- 

- def spawn_instance(spawn_playbook, log, timeout=None):

-     """

-     Spawn new VM, executing the following steps:

- 

-         - call the spawn playbook to startup/provision a building instance

-         - get an IP and test if the builder responds

-         - repeat this until you get an IP of working builder

- 

-     :type log: logging.Logger

-     :return: dict with ip and name of created VM

-     :raises CoprSpawnFailError:

-     """

-     log.info("Spawning a builder with pb: {}".format(spawn_playbook))

- 

-     start = time.time()

-     # Ansible playbook python API does not work here, dunno why.  See:

-     # https://groups.google.com/forum/#!topic/ansible-project/DNBD2oHv5k8

- 

-     spawn_args = "-c ssh {}".format(spawn_playbook)

-     try:

-         result = run_ansible_playbook_cli(spawn_args, comment="spawning instance", log=log, timeout=timeout)

-     except Exception as err:

-         raise CoprSpawnFailError(str(err.__dict__))

- 

-     if not result:

-         raise CoprSpawnFailError("No result, trying again")

- 

-     ipaddr = get_ip_from_log(result)

-     vm_name = get_vm_name_from_log(result)

- 

-     try:

-         IPAddress(ipaddr)

-     except:

-         # if we get here we are in trouble

-         msg = "Invalid IP: `{}` back from spawn_instance - dumping cache output\n".format(ipaddr)

-         msg += str(result)

-         raise CoprSpawnFailError(msg)

- 

-     log.info("Got VM {} ip: {}. Instance spawn/provision took {} sec"

-              .format(vm_name, ipaddr, time.time() - start))

-     return {"vm_ip": ipaddr, "vm_name": vm_name}

- 

- 

- def do_spawn_and_publish(opts, spawn_playbook, group):

- 

-     log = get_redis_logger(opts, "spawner.detached", "spawner")

- 

-     try:

-         log.debug("Going to spawn")

-         timeout = opts.build_groups[int(group)].get("playbook_timeout")

-         spawn_result = spawn_instance(spawn_playbook, log, timeout=timeout)

-         log.debug("Spawn finished")

-     except CoprSpawnFailError as err:

-         log.info("Spawning a builder with pb: %s", err.msg)

-         vm_ip = get_ip_from_log(err.msg)

-         vm_name = get_vm_name_from_log(err.msg)

-         if vm_ip and vm_name:

-             # VM started but failed later during ansible run.

-             try:

-                 log.exception("Trying to terminate: %s(%s).", vm_name, vm_ip)

-                 terminate.terminate_vm(opts, opts.build_groups[int(group)]["terminate_playbook"], group, vm_name, vm_ip)

-             except Exception:

-                 # ignore all errors

-                 raise

-         log.exception("Error during ansible invocation: %s", err.msg)

-         return

-     except Exception as err:

-         log.exception("[Unexpected] Failed to spawn builder: %s", err)

-         return

- 

-     spawn_result["group"] = group

-     spawn_result["topic"] = EventTopics.VM_SPAWNED

-     try:

-         rc = get_redis_connection(opts)

-         rc.publish(PUBSUB_MB, json.dumps(spawn_result))

-     except Exception as err:

-         log.exception("Failed to publish msg about new VM: %s with error: %s",

-                       spawn_result, err)

- 

- 

- class Spawner(Executor):

-     __name_for_log__ = "spawner"

-     __who_for_log__ = "spawner"

- 

-     def __init__(self, *args, **kwargs):

-         super(Spawner, self).__init__(*args, **kwargs)

-         self.proc_to_group = {}  # {proc: Thread -> group: int}

- 

-     def after_proc_finished(self, proc):

-         self.proc_to_group.pop(proc)

- 

-     def get_proc_num_per_group(self, group):

-         return sum(1 for _, gr in self.proc_to_group.items() if gr == group)

- 

-     def start_spawn(self, group):

-         try:

-             spawn_playbook = self.opts.build_groups[group]["spawn_playbook"]

-         except KeyError:

-             msg = "Config missing spawn playbook for group: {}".format(group)

-             raise CoprSpawnFailError(msg)

- 

-         if spawn_playbook is None:

-             msg = "Missing spawn playbook for group: {} for unknown reason".format(group)

-             raise CoprSpawnFailError(msg)

- 

-         if not os.path.exists(spawn_playbook):

-             msg = "Spawn playbook {} is missing".format(spawn_playbook)

-             raise CoprSpawnFailError(msg)

- 

-         proc = self.run_detached(do_spawn_and_publish, args=(self.opts, spawn_playbook, group))

-         self.proc_to_group[proc] = group

@@ -1,76 +0,0 @@

- # coding: utf-8

- import json

- import os

- import time

- from copr_backend.ans_utils import ans_extra_vars_encode, run_ansible_playbook_cli

- from copr_backend.exceptions import CoprSpawnFailError

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage import EventTopics, PUBSUB_MB

- from copr_backend.vm_manage.executor import Executor

- from ..helpers import get_redis_logger

- 

- 

- def terminate_vm(opts, terminate_playbook, group, vm_name, vm_ip):

-     """

-     Call the terminate playbook to destroy the instance

-     """

-     log = get_redis_logger(opts, "terminator.detached", "terminator")

- 

-     term_args = {"ip": vm_ip, "vm_name": vm_name}

- 

-     args = "-c ssh {} {}".format(

-         # self.vm_ip,

-         terminate_playbook,

-         ans_extra_vars_encode(term_args, "copr_task"))

- 

-     result = {

-         "vm_ip": vm_ip,

-         "vm_name": vm_name,

-         "group": group,

-         "topic": EventTopics.VM_TERMINATED,

-         "result": "OK"

-     }

-     start_time = time.time()

-     try:

-         log.info("starting terminate vm with args: %s", term_args)

-         run_ansible_playbook_cli(args, "terminate instance", log)

-         result["result"] = "OK"

-     except Exception as error:

-         result["result"] = "failed"

-         msg = "Failed to terminate an instance: vm_name={}, vm_ip={}, error: {}".format(vm_name, vm_ip, error)

-         result["msg"] = msg

-         log.exception(msg)

- 

-     try:

-         log.info("VM terminated %s, time elapsed: %s ", term_args, time.time() - start_time)

-         rc = get_redis_connection(opts)

-         rc.publish(PUBSUB_MB, json.dumps(result))

-     except Exception as error:

-         log.exception("Failed to publish msg about new VM: %s with error: %s", result, error)

- 

- 

- class Terminator(Executor):

- 

-     __name_for_log__ = "terminator"

-     __who_for_log__ = "terminator"

- 

-     def terminate_vm(self, vm_ip, vm_name, group):

-         self.recycle()

- 

-         try:

-             terminate_playbook = self.opts.build_groups[int(group)]["terminate_playbook"]

-         except KeyError:

-             msg = "Config missing termination playbook for group: {}".format(group)

-             raise CoprSpawnFailError(msg)

- 

-         if terminate_playbook is None:

-             msg = "Missing terminate playbook for group: {} for unknown reason".format(group)

-             raise CoprSpawnFailError(msg)

- 

-         if not os.path.exists(terminate_playbook):

-             msg = "Termination playbook {} is missing".format(terminate_playbook)

-             raise CoprSpawnFailError(msg)

- 

-         self.log.info("received VM ip: %s, name: %s for termination", vm_ip, vm_name)

- 

-         self.run_detached(terminate_vm, args=(self.opts, terminate_playbook, group, vm_name, vm_ip))

@@ -151,12 +151,6 @@

  group=root

  priority=901

  

- [program:copr-backend-vmm]

- command=/usr/bin/copr_run_vmm.py

- user=root

- group=root

- priority=902

- 

  [program:copr-backend-build]

  command=/usr/bin/copr-run-dispatcher builds

  user=root

@@ -1,159 +0,0 @@

- #!/usr/bin/python3

- # coding: utf-8

- 

- import os

- import sys

- import getpass

- import time

- import logging

- import argparse

- 

- from concurrent.futures import ThreadPoolExecutor, as_completed

- from dateutil.parser import parse as dt_parse

- 

- import yaml

- from novaclient.client import Client

- 

- # don't kill younger VMs than this (minutes)

- SPAWN_TIMEOUT = 10

- 

- from copr_backend.helpers import BackendConfigReader

- from copr_backend.helpers import utc_now

- 

- try:

-     from copr_backend.vm_manage.manager import VmManager

-     from copr_backend.vm_manage import VmStates

- except ImportError:

-     VmManager = None

- 

- logging.getLogger("requests").setLevel(logging.ERROR)

- 

- log = logging.getLogger(__name__)

- log.setLevel(logging.DEBUG)

- log_format = logging.Formatter('[%(asctime)s][%(thread)s][%(levelname)6s]: %(message)s')

- hfile = logging.FileHandler('/var/log/copr-backend/cleanup_vms.log')

- hfile.setLevel(logging.INFO)

- hstderr = logging.StreamHandler()

- hfile.setFormatter(log_format)

- log.addHandler(hfile)

- log.addHandler(hstderr)

- 

- 

- nova_cloud_vars_path = os.environ.get("NOVA_CLOUD_VARS", "/home/copr/provision/nova_cloud_vars.yml")

- 

- 

- def get_arg_parser():

-     parser = argparse.ArgumentParser(

-         description="Delete all errored or copr-managed VMs from relevant "

-                     "OpenStack tenant",

-     )

- 

-     parser.add_argument('--kill-also-unused', action='store_true',

-                         help='Delete also tracked, but unused VMs',

-                         default=False)

-     return parser

- 

- 

- def read_config():

-     with open(nova_cloud_vars_path) as handle:

-         conf = yaml.safe_load(handle.read())

-     return conf

- 

- 

- def get_client(conf):

-     username = conf["OS_USERNAME"]

-     password = conf["OS_PASSWORD"]

-     tenant_name = conf["OS_TENANT_NAME"]

-     auth_url = conf["OS_AUTH_URL"]

-     return Client('2', username, password, tenant_name, auth_url)

- 

- 

- def get_managed_vms():

-     result = {}

-     if VmManager:

-         opts = BackendConfigReader().read()

-         vmm = VmManager(opts, log)

-         for vmd in vmm.get_all_vm():

-             result[vmd.vm_name.lower()] = {

-                 'unused': vmd.state == VmStates.READY,

-             }

-     return result

- 

- 

- class Cleaner(object):

-     def __init__(self, conf):

-         self.conf = conf

-         self.nt = None

- 

-     @staticmethod

-     def terminate(srv):

-         try:

-             srv.delete()

-         except Exception:

-             log.exception("failed to request VM termination")

- 

-     @staticmethod

-     def old_enough(srv):

-         dt_created = dt_parse(srv.created)

-         delta = (utc_now() - dt_created).total_seconds()

-         if delta > 60 * SPAWN_TIMEOUT:

-             log.debug("Server '%s', created: %s, now: %s, delta: %s",

-                       srv.name, dt_created, utc_now(), delta)

-             return True

-         return False

- 

-     def check_one(self, srv_id, managed_vms, opts):

-         srv = self.nt.servers.get(srv_id)

-         log.debug("checking vm '%s'", srv.name)

-         srv.get()

- 

-         managed = managed_vms.get(srv.human_id.lower())

- 

-         if srv.status.lower().strip() == "error":

-             log.info("vm '%s' got into the error state, terminating", srv.name)

-             self.terminate(srv)

-         elif not managed:

-             if self.old_enough(srv): # give the spawner some time

-                 log.info("vm '%s' not placed in our db, terminating", srv.name)

-                 self.terminate(srv)

-         elif opts.kill_also_unused and managed['unused']:

-             log.info("terminating unused vm %s", srv.name)

-             self.terminate(srv)

- 

- 

-     def main(self, opts):

-         """

-         Terminate

-         - errored VM's and

-         - VM's with uptime > SPAWN_TIMEOUT minutes and which don't have entry in

-           redis DB

-         - when --kill-also-unused, we also terminate ready VMs

-         """

- 

-         if getpass.getuser() != 'copr':

-             log.error("This script needs to be executed as copr user")

-             sys.exit(1)

- 

-         start = time.time()

-         log.info("Cleanup start")

- 

-         self.nt = get_client(self.conf)

-         srv_list = self.nt.servers.list(detailed=False)

-         managed_vms = get_managed_vms()

-         with ThreadPoolExecutor(max_workers=20) as executor:

-             future_check = {

-                 executor.submit(self.check_one, srv.id, managed_vms, opts):

-                 srv.id for srv in srv_list

-             }

-             for future in as_completed(future_check):

-                 try:

-                     future.result()

-                 except Exception as exc:

-                     log.exception(exc)

- 

-         log.info("cleanup consumed: %s seconds", time.time() - start)

- 

- 

- if __name__ == "__main__":

-     cleaner = Cleaner(read_config())

-     cleaner.main(get_arg_parser().parse_args())

@@ -4,174 +4,7 @@

  Process one Build task provided by frontend (on backend).

  """

  

- import time

- 

- from copr_common.enums import StatusEnum

- 

- from copr_backend.background_worker import BackgroundWorker

- from copr_backend.job import BuildJob

- from copr_backend.daemons.worker import Worker

- from copr_backend.vm_alloc import ResallocHostFactory

- from copr_backend.cancellable_thread import CancellableThreadTask

- from copr_backend.sshcmd import SSHConnection, SSHConnectionError

- 

- 

- class BuildCanceled(Exception):

-     """ asynchronous cancel request received """

- 

- class DeadWorker(Exception):

-     """ this VM needs to be recycled """

- 

- 

- class BuildBackgroundWorker(BackgroundWorker):

-     """

-     The (S)RPM build logic.

-     """

- 

-     redis_logger_id = 'worker'

-     job = None

-     host = None

-     ssh = None

-     canceled = False

- 

-     @classmethod

-     def adjust_arg_parser(cls, parser):

-         parser.add_argument(

-             "--build-id",

-             type=int,

-             required=True,

-             help="build ID to process",

-         )

-         parser.add_argument(

-             "--chroot",

-             required=True,

-             help="chroot name (or 'srpm')",

-         )

- 

-     def mark_starting(self):

-         """

-         Announce to the frontend that the build is starting. Frontend may reject

-         build to start.

-         """

-         self.job.started_on = time.time()

-         self.job.status = StatusEnum("starting")

-         if not self.frontend_client.starting_build(self.job.to_dict()):

-             raise Exception("Frontend forbade to start the job {}".format(

-                 self.job.task_id))

- 

-     def get_build_job(self):

-         """

-         Per self.args, obtain BuildJob instance.

-         """

-         if self.args.chroot == "srpm-builds":

-             target = "get-srpm-build-task/{}".format(self.args.build_id)

-         else:

-             target = "get-build-task/{}-{}".format(self.args.build_id,

-                                                    self.args.chroot)

-         resp = self.frontend_client.get(target)

-         if resp.status_code != 200:

-             self.log.error("failed to download build info, apache code %s",

-                            resp.status_code)

-             raise Exception("failed to get the build task {}".format(target))

- 

-         self.job = BuildJob(resp.json(), self.opts)

- 

-     def _drop_host(self):

-         if not self.host:

-             return

-         self.host.release()

-         self.host = None

- 

-     def _title(self, text):

-         text = "Builder for task {}: {}".format(self.job.task_id, text)

-         self.log.debug("setting title: %s", text)

-         self.setproctitle(text)

- 

-     def _cancel_task_check_request(self):

-         self.canceled = bool(self.redis_get_worker_flag("cancel_request"))

-         return self.canceled

- 

-     def _alloc_host(self):

-         """

-         Set self.host with ready RemoteHost, and return True.  Keep re-trying

-         upon allocation failure.  Return False if the request was canceled.

-         """

-         tags = []

-         if self.job.arch:

-             tags.append("arch_{}".format(self.job.arch))

- 

-         vm_factory = ResallocHostFactory(server=self.opts.resalloc_connection)

-         while True:

-             self.host = vm_factory.get_host(tags, self.job.sandbox)

-             self._title("Waiting for VM, info: {}".format(self.host.info))

-             success = CancellableThreadTask(

-                 self.host.wait_ready,

-                 self._cancel_task_check_request,

-                 self._drop_host,

-             ).run()

-             if self.canceled:

-                 raise BuildCanceled

-             if success:

-                 return True

- 

-     def _alloc_ssh_connections(self):

-         self.ssh = SSHConnection(

-             user=self.opts.build_user,

-             host=self.host.hostname,

-             config_file=self.opts.ssh.builder_config

-         )

- 

-     def _cancel_running_worker(self):

-         self._title("Canceling running task...")

-         try:

-             cmd = "cat /var/lib/copr-rpmbuild/pid"

-             rc, out, err = self.ssh.run_expensive(cmd)

-             if rc:

-                 if "No such file" in err:

-                     self.log.warning("no PID file to cancel")

-                     return

-                 msg = "Can't get PID file to cancel {}".format(err)

-                 raise DeadWorker(msg)

-             pid = int(out.strip())

-             # TODO: kill -9 can keep mock around

-             self.log.info("killing PID %s on worker", pid)

-             self.ssh.run_expensive("kill -9 -{}".format(pid))

-         except ValueError:

-             raise DeadWorker("Can't parse PID to cancel")

-         except SSHConnectionError:

-             raise DeadWorker("Can't ssh to cancel build.")

- 

-     def _handle_task(self):

-         self.get_build_job()

-         self.mark_starting()

-         self._alloc_host()

-         self._alloc_ssh_connections()

-         self._title("Job {}, host info: {}".format(self.job.task_id,

-                                                    self.host.info))

-         worker = Worker(self.opts, self.worker_id, self.host, self.job)

-         CancellableThreadTask(

-             worker.run,

-             self._cancel_task_check_request,

-             self._cancel_running_worker,

-         ).run()

-         if self.canceled:

-             raise BuildCanceled

- 

-     def handle_task(self):

-         result = StatusEnum("failed")

- 

-         try:

-             result = self._handle_task()

-         except BuildCanceled:

-             self.log.error("build was canceled")

-             self.redis_set_worker_flag("fail_reason", "Build was canceled.")

-         except Exception:  # pylint: disable=broad-except

-             self.log.exception("unexpected exception")

-             self.redis_set_worker_flag("fail_reason", "Unexpected exception.")

-         finally:

-             self._drop_host()

-             self.redis_set_worker_flag('status', str(result))

- 

+ from copr_backend.background_worker_build import BuildBackgroundWorker

  

  if __name__ == "__main__":

      BuildBackgroundWorker().process()

file modified
+9 -6
@@ -9,16 +9,19 @@

  mess up everything around.

  """

  

- import os

- import sys

- 

  import argparse

  import contextlib

  import logging

- import oslo_concurrency.lockutils

+ import os

  import pipes

  import shutil

  import subprocess

+ import sys

+ 

+ import oslo_concurrency.lockutils

+ 

+ from copr_backend.helpers import get_redis_logger, BackendConfigReader

+ 

  

  class CommandException(Exception):

      pass
@@ -82,10 +85,10 @@

  

  

  def process_backend_config(opts):

-     # obtain backend options

      try:

-         from copr_backend.helpers import get_redis_logger, BackendConfigReader

          config = "/etc/copr/copr-be.conf"

+         if "COPR_BE_CONFIG" in os.environ:

+             config = os.environ["COPR_BE_CONFIG"]

          opts.backend_opts = BackendConfigReader(config).read()

          opts.results_baseurl = opts.backend_opts.results_baseurl

      except:

@@ -1,16 +0,0 @@

- #!/usr/bin/python3

- 

- # coding: utf-8

- 

- from copr_backend.helpers import BackendConfigReader

- from copr_backend.vm_manage.manager import VmManager

- 

- 

- def main():

-     opts = BackendConfigReader().read()

-     vmm = VmManager(opts, None)

-     print(vmm.info())

- 

- 

- if __name__ == "__main__":

-     main()

@@ -1,56 +0,0 @@

- #!/usr/bin/python3

- # coding: utf-8

- 

- from setproctitle import setproctitle

- 

- from copr_backend.vm_manage.manager import VmManager

- from copr_backend.vm_manage.spawn import Spawner

- from copr_backend.vm_manage.event_handle import EventHandler

- from copr_backend.vm_manage.terminate import Terminator

- from copr_backend.vm_manage.check import HealthChecker

- from copr_backend.daemons.vm_master import VmMaster

- from copr_backend.helpers import get_redis_logger, get_backend_opts

- 

- 

- class VmmRunner(object):

- 

-     def __init__(self, opts):

-         self.opts = opts

-         self.log = get_redis_logger(self.opts, "vmm.main", "vmm")

- 

-     def run(self):

-         # todo: 1) do all ansible calls through subprocess

-         # 2) move to Python 3 and asyncIO all in one thread + executors

-         # ... -> eliminate multiprocessing here,

-         # ... possible to use simple logging, with redis handler

- 

-         self.log.info("Creating VM Spawner, HealthChecker, Terminator")

-         self.spawner = Spawner(self.opts)

-         self.checker = HealthChecker(self.opts)

-         self.terminator = Terminator(self.opts)

-         self.vm_manager = VmManager(

-             opts=self.opts, logger=self.log,

-         )

-         self.log.info("Starting up VM EventHandler")

-         self.event_handler = EventHandler(self.opts,

-                                           vmm=self.vm_manager,

-                                           terminator=self.terminator)

-         self.event_handler.post_init()

-         self.event_handler.start()

- 

-         self.log.info("Starting up VM Master")

-         self.vm_master = VmMaster(self.opts,

-                                   vmm=self.vm_manager,

-                                   spawner=self.spawner,

-                                   checker=self.checker)

-         self.vm_master.start()

-         setproctitle("Copr VMM base process")

- 

- 

- def main():

-     opts = get_backend_opts()

-     vr = VmmRunner(opts)

-     vr.run()

- 

- if __name__ == "__main__":

-     main()

file modified
+17 -1
@@ -3,6 +3,21 @@

  set -x

  set -e

  

+ srcdir=$(dirname "$0")

+ 

+ test_tarball_version=$(grep -E '%global[[:space:]]*tests_version' < "$srcdir"/copr-backend.spec | awk '{ print $3 }')

+ test_tarball_name=$(grep -E '%global[[:space:]]*tests_tar' < "$srcdir"/copr-backend.spec | awk '{ print $3 }')

+ test_tarball_extracted=$test_tarball_name-$test_tarball_version

+ test_tarball=$test_tarball_extracted.tar.gz

+ 

+ test -d "$test_tarball_extracted" || (

+     cd "$srcdir" || exit 1

+     spectool -S copr-backend.spec --get-files

+     tar -xf "$test_tarball"

+ )

+ export TEST_DATA_DIRECTORY

+ TEST_DATA_DIRECTORY=$(readlink -f "$test_tarball_extracted")

+ 

  REDIS_PORT=7777

  redis-server --port $REDIS_PORT &> _redis.log &

  
@@ -14,7 +29,8 @@

  trap cleanup EXIT

  

  common_path=$(readlink -f ../common)

- export PYTHONPATH="$common_path:tests:run${PYTHONPATH+:$PYTHONPATH}"

+ export PYTHONPATH="$common_path:$PWD:$PWD/tests:$PWD/run${PYTHONPATH+:$PYTHONPATH}"

+ export PATH="$PWD/run${PATH+:$PATH}"

  

  COVPARAMS='--cov-report term-missing --cov ./copr_backend --cov ./run'

  

@@ -12,6 +12,9 @@

  

  from copr_backend.helpers import call_copr_repo

  

+ # We intentionally let fixtures depend on other fixtures.

+ # pylint: disable=redefined-outer-name

+ 

  class CoprTestFixtureContext():

      pass

  

@@ -1,520 +0,0 @@

- import json

- import multiprocessing

- import os

- import pprint

- from subprocess import CalledProcessError

- 

- from munch import Munch

- import pytest

- import tempfile

- import shutil

- import time

- 

- from copr_backend.constants import BuildStatus

- from copr_backend.exceptions import CoprWorkerError, CoprSpawnFailError, MockRemoteError, NoVmAvailable, VmError

- from copr_backend.job import BuildJob

- from copr_backend.vm_alloc import RemoteHost

- 

- from unittest import mock, skip

- from unittest.mock import MagicMock

- 

- from copr_backend.daemons.worker import Worker

- 

- # TODO: drop these, not needed

- JOB_GRAB_TASK_END_PUBSUB = "unused"

- 

- STDOUT = "stdout"

- STDERR = "stderr"

- COPR_OWNER = "copr_owner"

- COPR_NAME = "copr_name"

- COPR_VENDOR = "vendor"

- 

- MODULE_REF = "copr_backend.daemons.worker"

- 

- @pytest.yield_fixture

- def mc_register_build_result(*args, **kwargs):

-     patcher = mock.patch("{}.register_build_result".format(MODULE_REF))

-     obj = patcher.start()

-     yield obj

-     patcher.stop()

- 

- 

- @pytest.yield_fixture

- def mc_run_ans():

-     with mock.patch("{}.run_ansible_playbook".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_mr_class():

-     with mock.patch("{}.MockRemote".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_grl():

-     with mock.patch("{}.get_redis_logger".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- class TestDispatcher(object):

- 

-     def setup_method(self, method):

-         self.test_time = time.time()

-         subdir = "test_createrepo_{}".format(time.time())

-         self.tmp_dir_path = os.path.join(tempfile.gettempdir(), subdir)

-         os.mkdir(self.tmp_dir_path)

- 

-         self.pkg_pdn = "foobar"

-         self.pkg_name = "{}.src.rpm".format(self.pkg_pdn)

-         self.pkg_path = os.path.join(self.tmp_dir_path, self.pkg_name)

-         with open(self.pkg_path, "w") as handle:

-             handle.write("1")

- 

-         self.CHROOT = "fedora-20-x86_64"

-         self.vm_ip = "192.168.1.2"

-         self.vm_name = "VM_{}".format(self.test_time)

- 

-         self.DESTDIR = os.path.join(self.tmp_dir_path, COPR_OWNER, COPR_NAME)

-         self.DESTDIR_CHROOT = os.path.join(self.DESTDIR, self.CHROOT)

-         self.FRONT_URL = "htt://front.example.com"

-         self.BASE_URL = "http://example.com/results"

-         self.PKG_NAME = "foobar"

-         self.PKG_VERSION = "1.2.3"

-         self.HOST = "127.0.0.1"

-         self.SRC_PKG_URL = "http://example.com/{}-{}.src.rpm".format(self.PKG_NAME, self.PKG_VERSION)

-         self.job_build_id = 12345

- 

-         self.GIT_HASH = "1234r"

-         self.GIT_BRANCH = "f20"

-         self.GIT_REPO = "foo/bar/xyz"

- 

-         self.task = {

-             "project_owner": COPR_OWNER,

-             "project_name": COPR_NAME,

-             "pkgs": self.SRC_PKG_URL,

-             "repos": "",

-             "build_id": self.job_build_id,

-             "chroot": self.CHROOT,

-             "project_dirname": COPR_NAME,

-             "task_id": "{}-{}".format(self.job_build_id, self.CHROOT),

- 

-             "git_repo": self.GIT_REPO,

-             "git_hash": self.GIT_HASH,

-             "git_branch": self.GIT_BRANCH,

- 

-             "package_name": self.PKG_NAME,

-             "package_version": self.PKG_VERSION

-         }

- 

-         self.spawn_pb = "/spawn.yml"

-         self.terminate_pb = "/terminate.yml"

-         self.opts = Munch(

-             ssh=Munch(transport="paramiko"),

-             frontend_base_url="http://example.com",

-             frontend_auth="12345678",

-             build_groups={

-                 3: {

-                     "spawn_playbook": self.spawn_pb,

-                     "terminate_playbook": self.terminate_pb,

-                     "name": "3"

-                 }

-             },

- 

-             fedmsg_enabled=False,

-             sleeptime=0.1,

-             do_sign=True,

-             timeout=1800,

-             destdir=self.tmp_dir_path,

-             results_baseurl="/tmp",

- 

-             consecutive_failure_threshold=10,

- 

-             redis_host="127.0.0.1",

-             redis_port=6379,

-             redis_db=0,

-         )

-         self.job = BuildJob(self.task, self.opts)

- 

-         self.try_spawn_args = '-c ssh {}'.format(self.spawn_pb)

- 

-         self.worker_num = 2

-         self.group_id = 3

- 

-         self.ip = "192.168.1.1"

-         self.worker_callback = MagicMock()

- 

-         self.logfile_path = os.path.join(self.tmp_dir_path, "test.log")

- 

-         self.frontend_client = MagicMock()

- 

-     @pytest.yield_fixture

-     def mc_vmm(self):

-         with mock.patch("{}.VmManager".format(MODULE_REF)) as handle:

-             self.vmm = MagicMock()

-             handle.return_value = self.vmm

-             yield self.vmm

- 

-     @pytest.fixture

-     def init_worker(self):

-         vm = RemoteHost()

-         vm.hostname = "1.1.1.1"

- 

-         self.worker = Worker(

-             opts=self.opts,

-             worker_id=None,

-             vm=vm,

-             job=self.job,

-         )

- 

-         self.worker.vmm = MagicMock()

- 

-         def set_ip(*args, **kwargs):

-             self.worker.vm_ip = self.vm_ip

- 

-         def erase_ip(*args, **kwargs):

-             self.worker.vm_ip = None

- 

-         self.set_ip = set_ip

-         self.erase_ip = erase_ip

- 

-     @pytest.fixture

-     def reg_vm(self):

-         # call only with init_worker fixture

-         self.worker.vm_name = self.vm_name

-         self.worker.vm_ip = self.vm_ip

- 

-     def teardown_method(self, method):

-         return

-         # print("\nremove: {}".format(self.tmp_dir_path))

-         shutil.rmtree(self.tmp_dir_path)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_init_worker_wo_callback(self):

-         worker = Worker(

-             opts=self.opts,

-             frontend_client=self.frontend_client,

-             worker_num=self.worker_num,

-             group_id=self.group_id,

-         )

-         worker.vmm = MagicMock()

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_pkg_built_before(self):

-         assert not Worker.pkg_built_before(self.pkg_path, self.CHROOT, self.tmp_dir_path)

-         target_dir = os.path.join(self.tmp_dir_path, self.CHROOT, self.pkg_pdn)

-         os.makedirs(target_dir)

-         assert not Worker.pkg_built_before(self.pkg_path, self.CHROOT, self.tmp_dir_path)

-         with open(os.path.join(target_dir, "fail"), "w") as handle:

-             handle.write("undone")

-         assert not Worker.pkg_built_before(self.pkg_path, self.CHROOT, self.tmp_dir_path)

-         os.remove(os.path.join(target_dir, "fail"))

-         with open(os.path.join(target_dir, "success"), "w") as handle:

-             handle.write("done")

-         assert Worker.pkg_built_before(self.pkg_path, self.CHROOT, self.tmp_dir_path)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_mark_started(self, init_worker):

-         self.worker.mark_started(self.job)

-         assert self.frontend_client.update.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_mark_started_error(self, init_worker):

-         self.frontend_client.update.side_effect = IOError()

- 

-         with pytest.raises(CoprWorkerError):

-             self.worker.mark_started(self.job)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_return_results(self, init_worker):

-         self.job.started_on = self.test_time

-         self.job.ended_on = self.test_time + 10

- 

-         self.worker.mark_started(self.job)

- 

-         # expected_call = mock.call({'builds': [

-         #     {'status': 3, 'build_id': self.job_build_id,

-         #      'project_name': 'copr_name', 'submitter': None,

-         #      'project_owner': 'copr_owner', 'repos': [],

-         #      'results': u'/tmp/copr_owner/copr_name/',

-         #      'destdir': self.DESTDIR,

-         #      'started_on': self.job.started_on, 'submitted_on': None, 'chroot': 'fedora-20-x86_64',

-         #      'ended_on': self.job.ended_on, 'built_packages': '', 'timeout': 1800, 'pkg_version': '',

-         #      'pkg_epoch': None, 'pkg_main_version': '', 'pkg_release': None,

-         #      'memory_reqs': None, 'buildroot_pkgs': None, 'id': self.job_build_id,

-         #      'pkg': self.SRC_PKG_URL, "enable_net": True,

-         #      'task_id': self.job.task_id, 'mockchain_macros': {

-         #         'copr_username': 'copr_owner',

-         #         'copr_projectname': 'copr_name',

-         #         'vendor': 'Fedora Project COPR (copr_owner/copr_name)'}

-         #      }

-         # ]})

- 

-         assert self.frontend_client.update.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_return_results_error(self, init_worker):

-         self.job.started_on = self.test_time

-         self.job.ended_on = self.test_time + 10

-         self.frontend_client.update.side_effect = IOError()

- 

-         with pytest.raises(CoprWorkerError):

-             self.worker.return_results(self.job)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_starting_builds(self, init_worker):

-         self.job.started_on = self.test_time

-         self.job.ended_on = self.test_time + 10

- 

-         self.worker.starting_build(self.job)

- 

-         # expected_call = mock.call(self.job_build_id, self.CHROOT)

-         assert self.frontend_client.starting_build.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_starting_build_error(self, init_worker):

-         self.frontend_client.starting_build.side_effect = IOError()

- 

-         with pytest.raises(CoprWorkerError):

-             self.worker.starting_build(self.job)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.daemons.dispatcher.MockRemote")

-     @mock.patch("copr_backend.daemons.dispatcher.os")

-     def test_do_job_failure_on_mkdirs(self, mc_os, mc_mr, init_worker, reg_vm):

-         mc_os.path.exists.return_value = False

-         mc_os.makedirs.side_effect = IOError()

- 

-         self.worker.do_job(self.job)

-         assert self.job.status == BuildStatus.FAILURE

-         assert not mc_mr.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_do_job(self, mc_mr_class, init_worker, reg_vm, mc_register_build_result):

-         assert not os.path.exists(self.DESTDIR_CHROOT)

- 

-         self.worker.do_job(self.job)

-         assert self.job.status == BuildStatus.SUCCEEDED

-         assert os.path.exists(self.DESTDIR_CHROOT)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_do_job_updates_details(self, mc_mr_class, init_worker, reg_vm, mc_register_build_result):

-         assert not os.path.exists(self.DESTDIR_CHROOT)

-         mc_mr_class.return_value.build_pkg_and_process_results.return_value = {

-             "results": self.test_time,

-         }

- 

-         self.worker.do_job(self.job)

-         assert self.job.status == BuildStatus.SUCCEEDED

-         assert self.job.results == self.test_time

-         assert os.path.exists(self.DESTDIR_CHROOT)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_do_job_mr_error(self, mc_mr_class, init_worker,

-                              reg_vm, mc_register_build_result):

-         mc_mr_class.return_value.build_pkg_and_process_results.side_effect = MockRemoteError("foobar")

- 

-         self.worker.do_job(self.job)

-         assert self.job.status == BuildStatus.FAILURE

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_copy_mock_logs(self, mc_mr_class, init_worker, reg_vm, mc_register_build_result):

-         os.makedirs(self.job.results_dir)

-         for filename in ["build-00012345.log", "build-00012345.rsync.log"]:

-             open(os.path.join(self.job.chroot_dir, filename), "w")

- 

-         self.worker.copy_mock_logs(self.job)

-         assert set(os.listdir(self.job.results_dir)) == set(["rsync.log.gz", "mockchain.log.gz"])

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_copy_mock_logs_missing_files(self, mc_mr_class, init_worker, reg_vm, mc_register_build_result):

-         os.makedirs(self.job.results_dir)

-         self.worker.copy_mock_logs(self.job)

-         assert set(os.listdir(self.job.results_dir)) == set()

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_clean_previous_build_results(self, mc_mr_class, init_worker, reg_vm, mc_register_build_result):

-         os.makedirs(self.job.results_dir)

- 

-         files = ["fail", "foo.rpm", "build.log.gz", "root.log.gz", "state.log.gz"]

-         for filename in files:

-             open(os.path.join(self.job.results_dir, filename), "w")

- 

-         with open(os.path.join(self.job.results_dir, "build.info"), "w") as build_info:

-             build_info.writelines(["build_id=123\n", "builder_ip=<bar>"])

- 

-         self.worker.clean_result_directory(self.job)

-         backup_dir = os.path.join(os.path.join(self.job.results_dir, "prev_build_backup"))

-         assert os.path.isdir(backup_dir)

-         assert set(os.listdir(backup_dir)) == set(files[2:] + ["build.info"])

-         assert "foo.rpm" in os.listdir(self.job.results_dir)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.daemons.dispatcher.fedmsg")

-     def test_init_fedmsg(self, mc_fedmsg, init_worker):

-         self.worker.init_fedmsg()

-         assert not mc_fedmsg.init.called

-         self.worker.opts.fedmsg_enabled = True

-         self.worker.init_fedmsg()

-         assert mc_fedmsg.init.called

- 

-         mc_fedmsg.init.side_effect = KeyError()

-         self.worker.init_fedmsg()

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_obtain_job(self, init_worker):

-         mc_tq = MagicMock()

-         self.worker.task_queue = mc_tq

-         self.worker.starting_build = MagicMock()

- 

-         self.worker.jg.get_build = MagicMock(return_value=self.task)

-         obtained_job = self.worker.obtain_job()

-         assert obtained_job.__dict__ == self.job.__dict__

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_obtain_job_dequeue_type_error(self, init_worker):

-         mc_tq = MagicMock()

-         self.worker.task_queue = mc_tq

-         self.worker.starting_build = MagicMock()

-         self.worker.pkg_built_before = MagicMock()

-         self.worker.pkg_built_before.return_value = False

- 

-         mc_tq.dequeue.side_effect = TypeError()

-         assert self.worker.obtain_job() is None

-         assert not self.worker.starting_build.called

-         assert not self.worker.pkg_built_before.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_obtain_job_dequeue_none_result(self, init_worker):

-         mc_tq = MagicMock()

-         self.worker.task_queue = mc_tq

-         self.worker.starting_build = MagicMock()

-         self.worker.pkg_built_before = MagicMock()

-         self.worker.pkg_built_before.return_value = False

- 

-         mc_tq.dequeue.return_value = None

-         assert self.worker.obtain_job() is None

-         assert not self.worker.starting_build.called

-         assert not self.worker.pkg_built_before.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_dummy_run(self, init_worker, mc_time, mc_grl):

-         self.worker.init_fedmsg = MagicMock()

-         self.worker.run_cycle = MagicMock()

-         self.worker.update_process_title = MagicMock()

- 

-         def on_run_cycle(*args, **kwargs):

-             self.worker.kill_received = True

- 

-         self.worker.run_cycle.side_effect = on_run_cycle

-         self.worker.run()

- 

-         assert self.worker.init_fedmsg.called

- 

-         assert mc_grl.called

-         assert self.worker.run_cycle.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_dummy_notify_job_grab_about_task_end(self, init_worker):

-         self.worker.rc = MagicMock()

-         self.worker.notify_job_grab_about_task_end(self.job)

-         expected = json.dumps({

-             "action": "remove",

-             "build_id": 12345,

-             "chroot": "fedora-20-x86_64",

-             "task_id": "12345-fedora-20-x86_64"

-         })

-         assert self.worker.rc.publish.call_args == mock.call(JOB_GRAB_TASK_END_PUBSUB, expected)

- 

-         self.worker.notify_job_grab_about_task_end(self.job, True)

-         expected2 = json.dumps({

-             "action": "reschedule",

-             "build_id": 12345,

-             "chroot": "fedora-20-x86_64",

-             "task_id": "12345-fedora-20-x86_64"

-         })

-         assert self.worker.rc.publish.call_args == mock.call(JOB_GRAB_TASK_END_PUBSUB, expected2)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_run_cycle(self, init_worker, mc_time):

-         self.worker.update_process_title = MagicMock()

-         self.worker.obtain_job = MagicMock()

-         self.worker.do_job = MagicMock()

-         self.worker.notify_job_grab_about_task_end = MagicMock()

- 

-         self.worker.obtain_job.return_value = None

-         self.worker.run_cycle()

-         assert self.worker.obtain_job.called

-         assert mc_time.sleep.called

-         assert not mc_time.time.called

- 

-         vmd = RemoteHost()

-         vmd.ip = vmd.vm_ip = self.vm_ip

-         vmd.vm_name = self.vm_name

- 

-         self.worker.obtain_job.return_value = self.job

-         self.worker.vmm.acquire_vm.side_effect = [

-             IOError(),

-             None,

-             NoVmAvailable("foobar"),

-             vmd,

-         ]

- 

-         self.worker.run_cycle()

-         assert not self.worker.do_job.called

-         assert self.worker.notify_job_grab_about_task_end.called_once

-         assert self.worker.notify_job_grab_about_task_end.call_args[1]["do_reschedule"]

-         self.worker.notify_job_grab_about_task_end.reset_mock()

- 

-         ###  normal work

-         def on_release_vm(*args, **kwargs):

-             assert self.worker.vm_ip == self.vm_ip

-             assert self.worker.vm_name == self.vm_name

- 

-         self.worker.vmm.release_vm.side_effect = on_release_vm

-         self.worker.run_cycle()

-         assert self.worker.do_job.called_once

-         assert self.worker.notify_job_grab_about_task_end.called_once

-         assert not self.worker.notify_job_grab_about_task_end.call_args[1].get("do_reschedule")

- 

-         assert self.worker.vmm.release_vm.called

- 

-         self.worker.vmm.acquire_vm = MagicMock()

-         self.worker.vmm.acquire_vm.return_value = vmd

- 

-         ### handle VmError

-         self.worker.notify_job_grab_about_task_end.reset_mock()

-         self.worker.vmm.release_vm.reset_mock()

-         self.worker.do_job.side_effect = VmError("foobar")

-         self.worker.run_cycle()

- 

-         assert self.worker.notify_job_grab_about_task_end.call_args[1]["do_reschedule"]

-         assert self.worker.vmm.release_vm.called

- 

-         ### handle other errors

-         self.worker.notify_job_grab_about_task_end.reset_mock()

-         self.worker.vmm.release_vm.reset_mock()

-         self.worker.do_job.side_effect = IOError()

-         self.worker.run_cycle()

- 

-         assert self.worker.notify_job_grab_about_task_end.call_args[1]["do_reschedule"]

-         assert self.worker.vmm.release_vm.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_run_cycle_halt_on_can_start_job_false(self, init_worker):

-         self.worker.notify_job_grab_about_task_end = MagicMock()

-         self.worker.obtain_job = MagicMock()

-         self.worker.obtain_job.return_value = self.job

-         self.worker.starting_build = MagicMock()

-         self.worker.starting_build.return_value = False

-         self.worker.acquire_vm_for_job = MagicMock()

- 

-         self.worker.run_cycle()

-         assert self.worker.starting_build.called

-         assert not self.worker.acquire_vm_for_job.called

@@ -1,604 +0,0 @@

- # coding: utf-8

- 

- import copy

- 

- from collections import defaultdict

- import json

- from random import choice

- import types

- from munch import Munch

- import time

- from multiprocessing import Queue

- 

- 

- import tempfile

- import shutil

- import os

- 

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage import VmStates

- from copr_backend.vm_manage.manager import VmManager

- from copr_backend.daemons.vm_master import VmMaster

- from copr_backend.exceptions import VmError, VmSpawnLimitReached

- 

- from unittest import mock, skip

- from unittest.mock import patch, MagicMock

- import pytest

- 

- # TODO: drop these, these are not needed nowadays

- JOB_GRAB_TASK_END_PUBSUB = "unused"

- 

- 

- """

- REQUIRES RUNNING REDIS

- TODO: look if https://github.com/locationlabs/mockredis can be used

- """

- 

- MODULE_REF = "copr_backend.daemons.vm_master"

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_psutil():

-     with mock.patch("{}.psutil".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_setproctitle():

-     with mock.patch("{}.setproctitle".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- # @pytest.yield_fixture

- # def mc_time_vmm():

- #     with mock.patch("copr_backend.vm_manage.manager.time") as handle:

- #         yield handle

- 

- 

- class TestCallback(object):

-     def log(self, msg):

-         print(msg)

- 

- 

- class TestVmMaster(object):

- 

-     def setup_method(self, method):

-         self.vm_spawn_min_interval = 30

- 

-         self.opts = Munch(

-             redis_host="127.0.0.1",

-             redis_db=9,

-             redis_port=7777,

-             ssh=Munch(

-                 transport="ssh"

-             ),

-             build_groups_count=2,

-             build_groups={

-                 0: {

-                     "name": "base",

-                     "archs": ["i386", "x86_64"],

-                     "max_vm_total": 5,

-                     "max_spawn_processes": 3,

-                     "vm_spawn_min_interval": self.vm_spawn_min_interval,

-                     "vm_dirty_terminating_timeout": 120,

-                     "vm_health_check_period": 10,

-                     "vm_health_check_max_time": 60,

-                     "vm_terminating_timeout": 300,

-                 },

-                 1: {

-                     "name": "arm",

-                     "archs": ["armV7"],

-                     "vm_spawn_min_interval": self.vm_spawn_min_interval,

-                     "vm_dirty_terminating_timeout": 120,

-                     "vm_health_check_period": 10,

-                     "vm_health_check_max_time": 60,

-                     "vm_terminating_timeout": 300,

-                 }

-             },

- 

-             fedmsg_enabled=False,

-             sleeptime=0.1,

-             vm_cycle_timeout=10,

- 

- 

-         )

- 

-         self.queue = Queue()

- 

-         self.vm_ip = "127.0.0.1"

-         self.vm_name = "localhost"

-         self.group = 0

-         self.username = "bob"

- 

-         self.rc = get_redis_connection(self.opts)

-         self.ps = None

-         self.log_msg_list = []

- 

-         self.callback = TestCallback()

-         # checker = HealthChecker(self.opts, self.callback)

-         self.checker = MagicMock()

-         self.spawner = MagicMock()

-         self.terminator = MagicMock()

- 

-         self.mc_logger = MagicMock()

-         self.vmm = VmManager(self.opts, logger=self.mc_logger)

- 

-         self.event_handler = MagicMock()

-         self.vm_master = VmMaster(

-             self.opts,

-             self.vmm,

-             self.spawner,

-             self.checker,

-         )

-         self.vm_master.event_handler = MagicMock()

-         self.pid = 12345

- 

-         self.vm_ip = "127.0.0.1"

-         self.vm_name = "build 12345"

- 

-     def clean_redis(self):

-         keys = self.vmm.rc.keys("*")

-         if keys:

-             self.vmm.rc.delete(*keys)

- 

-     def teardown_method(self, method):

-         self.clean_redis()

- 

-     @pytest.fixture

-     def add_vmd(self):

-         self.vmd_a1 = self.vmm.add_vm_to_pool("127.0.0.1", "a1", 0)

-         self.vmd_a2 = self.vmm.add_vm_to_pool("127.0.0.2", "a2", 0)

-         self.vmd_a3 = self.vmm.add_vm_to_pool("127.0.0.3", "a3", 0)

-         self.vmd_b1 = self.vmm.add_vm_to_pool("127.0.0.4", "b1", 1)

-         self.vmd_b2 = self.vmm.add_vm_to_pool("127.0.0.5", "b2", 1)

-         self.vmd_b3 = self.vmm.add_vm_to_pool("127.0.0.6", "b3", 1)

- 

-     def rcv_from_ps_message_bus(self):

-         # don't forget to subscribe self.ps

-         rcv_msg_list = []

-         for i in range(10):

-             msg = self.ps.get_message()

-             if msg:

-                 rcv_msg_list.append(msg)

-             time.sleep(0.01)

-         return rcv_msg_list

- 

-     def test_pass(self, add_vmd):

-         pass

- 

-     def test_remove_old_dirty_vms(self, mc_time, add_vmd):

-         # pass

-         self.vmm.start_vm_termination = types.MethodType(MagicMock(), self.vmm)

-         # VM in ready state, with not empty bount_to_user and (NOW - last_release) > threshold

-         #   should be terminated

-         for vmd in [self.vmd_a1, self.vmd_a2, self.vmd_b1, self.vmd_b2]:

-             vmd.store_field(self.rc, "state", VmStates.READY)

- 

-         for vmd in [self.vmd_a1, self.vmd_a2, self.vmd_b1]:

-             vmd.store_field(self.rc, "last_release", 0)

- 

-         for vmd in [self.vmd_a1, self.vmd_b1, self.vmd_b2]:

-             vmd.store_field(self.rc, "bound_to_user", "user")

- 

-         for vmd in [self.vmd_a3, self.vmd_b3]:

-             vmd.store_field(self.rc, "state", VmStates.IN_USE)

- 

-         mc_time.time.return_value = 1

-         # no vm terminated

-         self.vm_master.remove_old_dirty_vms()

-         assert not self.vmm.start_vm_termination.called

- 

-         mc_time.time.return_value = self.opts.build_groups[0]["vm_dirty_terminating_timeout"] + 1

- 

-         # only "a1" and "b1" should be terminated

-         self.vm_master.remove_old_dirty_vms()

-         assert self.vmm.start_vm_termination.called

-         terminated_names = set([call[0][1] for call

-                                in self.vmm.start_vm_termination.call_args_list])

-         assert set(["a1", "b1"]) == terminated_names

- 

-     def disabled_test_remove_vm_with_dead_builder(self, mc_time, add_vmd, mc_psutil):

-         # todo: re-enable after psutil.Process.cmdline will be in use

-         mc_time.time.return_value = time.time()

-         self.vm_master.log = MagicMock()

- 

-         self.vmm.start_vm_termination = MagicMock()

-         self.vmm.start_vm_termination.return_value = "OK"

- 

-         for idx, vmd in enumerate([self.vmd_a1, self.vmd_a2,

-                                    self.vmd_b1, self.vmd_b2, self.vmd_b3]):

-             vmd.store_field(self.rc, "state", VmStates.IN_USE)

-             vmd.store_field(self.rc, "chroot", "fedora-20-x86_64")

-             vmd.store_field(self.rc, "task_id", "{}-fedora-20-x86_64".format(idx + 1))

-             vmd.store_field(self.rc, "build_id", idx + 1)

-             vmd.store_field(self.rc, "in_use_since", 0)

- 

-         self.rc.hdel(self.vmd_b3.vm_key, "chroot")

- 

-         for idx, vmd in enumerate([self.vmd_a1, self.vmd_a2, self.vmd_b2, self.vmd_b3]):

-             vmd.store_field(self.rc, "used_by_worker", idx + 1)

- 

-         for vmd in [self.vmd_a3, self.vmd_a3]:

-             vmd.store_field(self.rc, "state", VmStates.READY)

- 

-         def mc_psutil_process(pid):

-             p = MagicMock()

-             mapping = {

-                 "1": "a1",

-                 "2": "a2",

-                 "3": "b1",

-                 "4": "None",

-                 "5": "None",

-             }

-             p.cmdline = ["builder vm_name={} suffix".format(mapping.get(str(pid))),]

-             return p

- 

-         def mc_psutil_pid_exists(pid):

-             if str(pid) in ["1", "4"]:

-                 return True

- 

-             return False

- 

-         mc_psutil.Process.side_effect = mc_psutil_process

-         mc_psutil.pid_exists.side_effect = mc_psutil_pid_exists

- 

-         self.ps = self.vmm.rc.pubsub(ignore_subscribe_messages=True)

-         self.ps.subscribe(JOB_GRAB_TASK_END_PUBSUB)

-         self.vm_master.remove_vm_with_dead_builder()

- 

-         msg_list = self.rcv_from_ps_message_bus()

-         assert self.vmm.start_vm_termination.call_args_list == [

-             mock.call('a2', allowed_pre_state='in_use'),

-             mock.call('b2', allowed_pre_state='in_use'),

-             mock.call('b3', allowed_pre_state='in_use')

-         ]

-         # changed logic for the moment

-         # assert set(["2", "4"]) == set([json.loads(m["data"])["build_id"] for m in msg_list])

- 

-     def test_check_vms_health(self, mc_time, add_vmd):

-         self.vm_master.start_vm_check = types.MethodType(MagicMock(), self.vmm)

-         for vmd in [self.vmd_a1, self.vmd_a2, self.vmd_a3, self.vmd_b1, self.vmd_b2, self.vmd_b3]:

-             vmd.store_field(self.rc, "last_health_check", 0)

- 

-         self.vmd_a1.store_field(self.rc, "state", VmStates.IN_USE)

-         self.vmd_a2.store_field(self.rc, "state", VmStates.CHECK_HEALTH)

-         self.vmd_a3.store_field(self.rc, "state", VmStates.CHECK_HEALTH_FAILED)

-         self.vmd_b1.store_field(self.rc, "state", VmStates.GOT_IP)

-         self.vmd_b2.store_field(self.rc, "state", VmStates.READY)

-         self.vmd_b3.store_field(self.rc, "state", VmStates.TERMINATING)

- 

-         mc_time.time.return_value = 1

-         self.vm_master.check_vms_health()

-         assert not self.vm_master.start_vm_check.called

- 

-         mc_time.time.return_value = 1 + self.opts.build_groups[0]["vm_health_check_period"]

-         self.vm_master.check_vms_health()

-         to_check = set(call[0][1] for call in self.vm_master.start_vm_check.call_args_list)

-         assert set(['a1', 'a3', 'b1', 'b2']) == to_check

- 

-         self.vm_master.start_vm_check.reset_mock()

-         for vmd in [self.vmd_a1, self.vmd_a2, self.vmd_a3, self.vmd_b1, self.vmd_b2, self.vmd_b3]:

-             self.rc.hdel(vmd.vm_key, "last_health_check")

- 

-         self.vm_master.check_vms_health()

-         to_check = set(call[0][1] for call in self.vm_master.start_vm_check.call_args_list)

-         assert set(['a1', 'a3', 'b1', 'b2']) == to_check

- 

-     def test_finalize_long_health_checks(self, mc_time, add_vmd):

- 

-         mc_time.time.return_value = 0

-         self.vmd_a1.store_field(self.rc, "state", VmStates.IN_USE)

-         self.vmd_a2.store_field(self.rc, "state", VmStates.CHECK_HEALTH)

-         self.vmd_a3.store_field(self.rc, "state", VmStates.CHECK_HEALTH)

- 

-         self.vmd_a2.store_field(self.rc, "last_health_check", 0)

-         self.vmd_a3.store_field(self.rc, "last_health_check",

-                                 self.opts.build_groups[0]["vm_health_check_max_time"] + 10)

- 

-         mc_time.time.return_value = self.opts.build_groups[0]["vm_health_check_max_time"] + 11

- 

-         self.vmm.mark_vm_check_failed = MagicMock()

-         self.vm_master.finalize_long_health_checks()

-         assert self.vmm.mark_vm_check_failed.called_once

-         assert self.vmm.mark_vm_check_failed.call_args[0][0] == "a2"

- 

-     def test_terminate_again(self, mc_time, add_vmd):

-         mc_time.time.return_value = 0

-         self.vmd_a1.store_field(self.rc, "state", VmStates.IN_USE)

-         self.vmd_a2.store_field(self.rc, "state", VmStates.CHECK_HEALTH)

-         self.vmd_a3.store_field(self.rc, "state", VmStates.READY)

- 

-         mc_time.time.return_value = 1

-         self.vmm.remove_vm_from_pool = MagicMock()

-         self.vmm.start_vm_termination = MagicMock()

-         # case 1 no VM in terminating states =>

-         #   no start_vm_termination, no remove_vm_from_pool

-         # import ipdb; ipdb.set_trace()

-         self.vm_master.terminate_again()

-         assert not self.vmm.remove_vm_from_pool.called

-         assert not self.vmm.start_vm_termination.called

- 

-         # case 2: one VM in terminating state with unique ip, time_elapsed < threshold

-         #   no start_vm_termination, no remove_vm_from_pool

-         self.vmd_a1.store_field(self.rc, "state", VmStates.TERMINATING)

-         self.vmd_a1.store_field(self.rc, "terminating_since", 0)

- 

-         self.vm_master.terminate_again()

-         assert not self.vmm.remove_vm_from_pool.called

-         assert not self.vmm.start_vm_termination.called

- 

-         # case 3: one VM in terminating state with unique ip, time_elapsed > threshold

-         #   start_vm_termination called, no remove_vm_from_pool

-         mc_time.time.return_value = 1 + self.opts.build_groups[0]["vm_terminating_timeout"]

- 

-         self.vm_master.terminate_again()

-         assert not self.vmm.remove_vm_from_pool.called

-         assert self.vmm.start_vm_termination.called

-         assert self.vmm.start_vm_termination.call_args[0][0] == self.vmd_a1.vm_name

- 

-         self.vmm.start_vm_termination.reset_mock()

- 

-         # case 4: two VM with the same IP, one in terminating states, , time_elapsed < threshold

-         #   no start_vm_termination, no remove_vm_from_pool

-         mc_time.time.return_value = 1

-         self.vmd_a2.store_field(self.rc, "vm_ip", self.vmd_a1.vm_ip)

- 

-         self.vm_master.terminate_again()

-         assert not self.vmm.remove_vm_from_pool.called

-         assert not self.vmm.start_vm_termination.called

- 

-         # case 4: two VM with the same IP, one in terminating states, , time_elapsed > threshold

-         #   no start_vm_termination, remove_vm_from_pool

-         mc_time.time.return_value = 1 + self.opts.build_groups[0]["vm_terminating_timeout"]

-         self.vm_master.terminate_again()

-         assert self.vmm.remove_vm_from_pool.called

-         assert self.vmm.remove_vm_from_pool.call_args[0][0] == self.vmd_a1.vm_name

-         assert not self.vmm.start_vm_termination.called

- 

-     # def test_run_undefined_helpers(self, mc_setproctitle):

-     #     for target in ["spawner", "terminator", "checker"]:

-     #         setattr(self.vmm, target, None)

-     #         with pytest.raises(RuntimeError):

-     #             self.vm_master.run()

-     #

-     #         setattr(self.vmm, target, MagicMock())

-     #

-     #         assert not mc_setproctitle.called

- 

-     def test_dummy_run(self, mc_time, mc_setproctitle):

-         mc_do_cycle = MagicMock()

-         mc_do_cycle.side_effect = [

-             VmError("FooBar"),

-             None

-         ]

-         self.vm_master.do_cycle = types.MethodType(mc_do_cycle, self.vm_master)

-         self.vmm.mark_server_start = MagicMock()

- 

-         self.stage = 0

- 

-         def on_sleep(*args, **kwargs):

-             self.stage += 1

-             if self.stage == 1:

-                 pass

-             elif self.stage >= 2:

-                 self.vm_master.kill_received = True

- 

-         mc_time.sleep.side_effect = on_sleep

-         self.vm_master.run()

- 

-     def test_dummy_terminate(self):

-         self.vm_master.terminate()

-         assert self.vm_master.kill_received

-         assert self.vm_master.checker.terminate.called

-         assert self.vm_master.spawner.terminate.called

- 

-     def test_dummy_do_cycle(self):

-         self.vm_master.remove_old_dirty_vms = types.MethodType(MagicMock(), self.vm_master)

-         self.vm_master.check_vms_health = types.MethodType(MagicMock(), self.vm_master)

-         self.vm_master.start_spawn_if_required = types.MethodType(MagicMock(), self.vm_master)

-         # self.vm_master.remove_old_dirty_vms = types(MagicMock, self.vm_master)

- 

-         self.vm_master.do_cycle()

- 

-         assert self.vm_master.remove_old_dirty_vms.called

-         assert self.vm_master.check_vms_health.called

-         assert self.vm_master.start_spawn_if_required.called

-         assert self.vm_master.spawner.recycle.called

- 

-     def test_dummy_start_spawn_if_required(self):

-         self.vm_master.try_spawn_one = MagicMock()

-         self.vm_master.start_spawn_if_required()

-         assert self.vm_master.try_spawn_one.call_args_list == [

-             mock.call(group) for group in range(self.opts.build_groups_count)

-         ]

- 

-     def test__check_total_running_vm_limit_raises(self):

-         self.vm_master.log = MagicMock()

-         active_vm_states = [VmStates.GOT_IP, VmStates.READY, VmStates.IN_USE, VmStates.CHECK_HEALTH]

-         cases = [

-             # active_vms_number , spawn_procs_number

-             (x, 11 - x) for x in range(12)

-         ]

-         self.opts.build_groups[0]["max_vm_total"] = 11

-         for active_vms_number, spawn_procs_number in cases:

-             vmd_list = [

-                 self.vmm.add_vm_to_pool("127.0.0.{}".format(idx + 1), "a{}".format(idx), 0)

-                 for idx in range(active_vms_number)

-             ]

-             for idx in range(active_vms_number):

-                 state = choice(active_vm_states)

-                 vmd_list[idx].store_field(self.rc, "state", state)

- 

-             self.vm_master.spawner.get_proc_num_per_group.return_value = spawn_procs_number

-             with pytest.raises(VmSpawnLimitReached):

-                 self.vm_master._check_total_running_vm_limit(0)

-             self.vm_master.log.reset_mock()

- 

-             # teardown

-             self.clean_redis()

- 

-     def test__check_total_running_vm_limit_ok(self):

-         self.vm_master.log = MagicMock()

-         active_vm_states = [VmStates.GOT_IP, VmStates.READY, VmStates.IN_USE, VmStates.CHECK_HEALTH]

-         cases = [

-             # active_vms_number , spawn_procs_number

-             (x, 11 - x) for x in range(12)

-         ]

-         self.opts.build_groups[0]["max_vm_total"] = 12

-         for active_vms_number, spawn_procs_number in cases:

-             vmd_list = [

-                 self.vmm.add_vm_to_pool("127.0.0.{}".format(idx + 1), "a{}".format(idx), 0)

-                 for idx in range(active_vms_number)

-             ]

-             for idx in range(active_vms_number):

-                 state = choice(active_vm_states)

-                 vmd_list[idx].store_field(self.rc, "state", state)

-             # self.vmm.spawner.children_number = spawn_procs_number

-             self.vm_master.spawner.get_proc_num_per_group.return_value = spawn_procs_number

- 

-             # doesn't raise exception

-             self.vm_master._check_total_running_vm_limit(0)

-             self.vm_master.log.reset_mock()

- 

-             # teardown

-             self.clean_redis()

- 

-     def test__check_elapsed_time_after_spawn(self, mc_time):

-         # don't start new spawn if last_spawn_time was in less self.vm_spawn_min_interval ago

-         mc_time.time.return_value = 0

-         self.vm_master._check_elapsed_time_after_spawn(0)

- 

-         self.vm_master.vmm.write_vm_pool_info(0, "last_vm_spawn_start", 0)

-         with pytest.raises(VmSpawnLimitReached):

-             self.vm_master._check_elapsed_time_after_spawn(0)

- 

-         mc_time.time.return_value = -1 + self.vm_spawn_min_interval

-         with pytest.raises(VmSpawnLimitReached):

-             self.vm_master._check_elapsed_time_after_spawn(0)

- 

-         mc_time.time.return_value = 1 + self.vm_spawn_min_interval

-         self.vm_master._check_elapsed_time_after_spawn(0)

-         # we don't care about other group

-         self.vm_master.vmm.write_vm_pool_info(1, "last_vm_spawn_start", mc_time.time.return_value)

-         self.vm_master._check_elapsed_time_after_spawn(0)

-         with pytest.raises(VmSpawnLimitReached):

-             self.vm_master._check_elapsed_time_after_spawn(1)

- 

-     def test__check_number_of_running_spawn_processes(self):

-         for i in range(self.opts.build_groups[0]["max_spawn_processes"]):

-             self.vm_master.spawner.get_proc_num_per_group.return_value = i

-             self.vm_master._check_number_of_running_spawn_processes(0)

- 

-         for i in [0, 1, 2, 5, 100]:

-             self.vm_master.spawner.get_proc_num_per_group.return_value = \

-                 self.opts.build_groups[0]["max_spawn_processes"] + i

- 

-             with pytest.raises(VmSpawnLimitReached):

-                 self.vm_master._check_number_of_running_spawn_processes(0)

- 

-     def test__check_total_vm_limit(self):

-         self.vm_master.vmm = MagicMock()

-         for i in range(2 * self.opts.build_groups[0]["max_vm_total"]):

-             self.vm_master.vmm.get_all_vm_in_group.return_value = [1 for _ in range(i)]

-             self.vm_master._check_total_vm_limit(0)

- 

-         for i in range(2 * self.opts.build_groups[0]["max_vm_total"],

-                        2 * self.opts.build_groups[0]["max_vm_total"] + 10):

-             self.vm_master.vmm.get_all_vm_in_group.return_value = [1 for _ in range(i)]

-             with pytest.raises(VmSpawnLimitReached):

-                 self.vm_master._check_total_vm_limit(0)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_try_spawn_error_handling(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vm_master.log = MagicMock()

- 

-         self.vm_master.spawner.start_spawn.side_effect = IOError()

- 

-         self.vm_master.try_spawn_one(0)

-         assert self.vm_master.spawner.start_spawn.called

- 

-     def test_try_spawn_exit_on_check_fail(self):

-         check_mocks = []

-         for check_name in [

-             "_check_total_running_vm_limit",

-             "_check_elapsed_time_after_spawn",

-             "_check_number_of_running_spawn_processes",

-             "_check_total_vm_limit",

-         ]:

-             mc_check_func = MagicMock()

-             check_mocks.append(mc_check_func)

-             setattr(self.vm_master, check_name, mc_check_func)

- 

-         self.vm_master.vmm = MagicMock()

-         for idx in range(len(check_mocks)):

-             for cm in check_mocks:

-                 cm.side_effect = None

-             check_mocks[idx].side_effect = VmSpawnLimitReached("test")

- 

-             self.vm_master.try_spawn_one(0)

-             assert not self.vm_master.vmm.write_vm_pool_info.called

-             self.vm_master.vmm.write_vm_pool_info.reset_mock()

- 

-         for cm in check_mocks:

-             cm.side_effect = None

- 

-         self.vm_master.try_spawn_one(0)

-         assert self.vm_master.vmm.write_vm_pool_info.called

-         assert self.vm_master.spawner.start_spawn.called

- 

-     def test_start_vm_check_ok_ok(self):

-         self.vmm.start_vm_termination = types.MethodType(MagicMock(), self.vmm)

-         self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, self.group)

-         vmd = self.vmm.get_vm_by_name(self.vm_name)

-         # can start, no problem to start

-         # > can start IN_USE, don't change status

-         vmd.store_field(self.rc, "state", VmStates.IN_USE)

-         self.vm_master.start_vm_check(vm_name=self.vm_name)

- 

-         assert self.checker.run_check_health.called

-         self.checker.run_check_health.reset_mock()

-         assert vmd.get_field(self.rc, "state") == VmStates.IN_USE

- 

-         # > changes status to HEALTH_CHECK

-         states = [VmStates.GOT_IP, VmStates.CHECK_HEALTH_FAILED, VmStates.READY]

-         for state in states:

-             vmd.store_field(self.rc, "state", state)

-             self.vm_master.start_vm_check(vm_name=self.vm_name)

- 

-             assert self.checker.run_check_health.called

-             self.checker.run_check_health.reset_mock()

-             assert vmd.get_field(self.rc, "state") == VmStates.CHECK_HEALTH

- 

-     def test_start_vm_check_wrong_old_state(self):

-         self.vmm.start_vm_termination = types.MethodType(MagicMock(), self.vmm)

-         self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, self.group)

-         vmd = self.vmm.get_vm_by_name(self.vm_name)

- 

-         states = [VmStates.TERMINATING, VmStates.CHECK_HEALTH]

-         for state in states:

-             vmd.store_field(self.rc, "state", state)

-             assert not self.vm_master.start_vm_check(vm_name=self.vm_name)

- 

-             assert not self.checker.run_check_health.called

-             assert vmd.get_field(self.rc, "state") == state

- 

-     def test_start_vm_check_lua_ok_check_spawn_failed(self):

-         self.vmm.start_vm_termination = types.MethodType(MagicMock(), self.vmm)

-         self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, self.group)

-         vmd = self.vmm.get_vm_by_name(self.vm_name)

- 

-         self.vm_master.checker.run_check_health.side_effect = RuntimeError()

- 

-         # restore orig state

-         states = [VmStates.GOT_IP, VmStates.CHECK_HEALTH_FAILED, VmStates.READY, VmStates.IN_USE]

-         for state in states:

-             vmd.store_field(self.rc, "state", state)

-             self.vm_master.start_vm_check(vm_name=self.vm_name)

- 

-             assert self.checker.run_check_health.called

-             self.checker.run_check_health.reset_mock()

-             assert vmd.get_field(self.rc, "state") == state

@@ -0,0 +1,5 @@

+ #! /bin/bash

+ 

+ if test x"$1" = "x-u"; then

+     echo "fake pub key content"

+ fi

@@ -1,802 +0,0 @@

- # coding: utf-8

- import copy

- 

- from collections import defaultdict

- from pprint import pprint

- import socket

- from munch import Munch

- from copr_backend.exceptions import BuilderError, VmError

- 

- import tempfile

- import shutil

- import os

- 

- from copr_backend.job import BuildJob

- 

- from unittest import mock, skip

- from unittest.mock import patch, MagicMock

- import pytest

- from types import MethodType

- 

- import copr_backend.mockremote.builder as builder_module

- from copr_backend.mockremote.builder import Builder

- 

- MODULE_REF = "copr_backend.mockremote.builder"

- 

- # TODO: drop these, these are not needed

- class BuilderTimeOutError(Exception):

-     pass

- class AnsibleCallError(Exception):

-     pass

- class AnsibleResponseError(Exception):

-     pass

- 

- 

- @pytest.yield_fixture

- def mc_socket():

-     yield object()

- 

- 

- def noop(*args, **kwargs):

-     pass

- 

- 

- def print_all(*args, **kwargs):

-     pprint(args)

-     pprint(kwargs)

- 

- 

- STDOUT = "stdout"

- STDERR = "stderr"

- COPR_OWNER = "copr_owner"

- COPR_NAME = "copr_name"

- COPR_VENDOR = "vendor"

- 

- 

- class TestBuilder(object):

-     BUILDER_BUILDROOT_PKGS = []

-     BUILDER_CHROOT = "fedora-20-i386"

-     BUILDER_TIMEOUT = 1024

-     BUILDER_HOSTNAME = "example.com"

-     BUILDER_USER = "copr_builder"

-     BUILDER_REMOTE_BASEDIR = "/tmp/copr-backend-test"

-     BUILDER_REMOTE_TMPDIR = "/tmp/copr-backend-test-tmp"

-     BUILDER_PKG_NAME = "foovar"

-     BUILDER_PKG_BASE = "foovar-2.41.f21"

-     BUILDER_PKG_VERSION = "2.41.f21"

-     BUILDER_PKG = "http://example.com/foovar-2.41.f21.src.rpm"

- 

-     BUILD_REMOTE_TARGET = "/tmp/copr-backend-test/foovar-2.41.f21.src.rpm"

- 

-     STDOUT = "stdout"

-     STDERR = "stderr"

- 

-     RESULT_DIR = "/tmp"

-     opts = Munch(

-         ssh=Munch(

-             transport="paramiko"

-         ),

-         build_user=BUILDER_USER,

-         timeout=BUILDER_TIMEOUT,

-         remote_basedir=BUILDER_REMOTE_BASEDIR,

-         remote_tempdir=BUILDER_REMOTE_TMPDIR,

-         results_baseurl="http://example.com",

- 

-         redis_db=9,

-         redis_port=7777,

-     )

- 

-     GIT_HASH = "1234r"

-     GIT_BRANCH = "f20"

-     GIT_REPO = "foo/bar/xyz"

- 

-     def get_test_builder(self):

-         self.job = BuildJob({

-             "project_owner": COPR_OWNER,

-             "project_name": COPR_NAME,

-             "pkgs": self.BUILDER_PKG,

-             "repos": "",

-             "build_id": 12345,

-             "chroot": self.BUILDER_CHROOT,

-             "buildroot_pkgs": self.BUILDER_BUILDROOT_PKGS,

- 

-             "git_repo": self.GIT_REPO,

-             "git_hash": self.GIT_HASH,

-             "git_branch": self.GIT_BRANCH,

- 

-             "package_name": self.BUILDER_PKG_NAME,

-             "package_version": self.BUILDER_PKG_VERSION

-         }, Munch({

-             "timeout": 1800,

-             "destdir": self.test_root_path,

-             "results_baseurl": "/tmp",

-         }))

- 

-         self.mc_logger = MagicMock()

-         builder = Builder(

-             opts=self.opts,

-             hostname=self.BUILDER_HOSTNAME,

-             job=self.job,

-             logger=self.mc_logger

-         )

-         builder.checked = True

- 

-         builder.remote_pkg_name = self.BUILDER_PKG_BASE

-         builder.remote_pkg_path = os.path.join(self.BUILDER_REMOTE_BASEDIR, self.BUILDER_PKG_BASE + ".src.rpm")

- 

-         return builder

- 

-     def setup_method(self, method):

-         self.test_root_path = tempfile.mkdtemp()

-         self.stage = 0

-         self.stage_ctx = defaultdict(dict)

- 

-     @property

-     def buildcmd(self):

-         return self.gen_mockchain_command(self.BUILDER_PKG)

- 

-     def teardown_method(self, method):

-         if os.path.exists(self.test_root_path):

-             shutil.rmtree(self.test_root_path)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_constructor(self):

-         builder = self.get_test_builder()

-         assert builder.conn.remote_user == self.BUILDER_USER

-         assert builder.root_conn.remote_user == "root"

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_get_remote_pkg_dir(self):

-         builder = self.get_test_builder()

-         expected = "/".join([self.BUILDER_REMOTE_TMPDIR, "build", "results",

-                              self.BUILDER_CHROOT, builder.remote_pkg_name])

-         assert builder._get_remote_results_dir() == expected

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_run_ansible(self):

-         builder = self.get_test_builder()

-         ans_cmd = "foo bar"

- 

-         for conn, as_root in [(builder.conn, False), (builder.root_conn, True)]:

-             for module_name in [None, "foo", "copy"]:

-                 run_count = conn.run.call_count

-                 builder._run_ansible(ans_cmd, as_root=as_root)

-                 assert conn.run.call_count == run_count + 1

-                 assert conn.module_args == ans_cmd

-                 assert conn.module_name == module_name or "shell"

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_for_ans_answer(self):

-         """

-             Silly test. Ansible api has almost no documentation,

-             so we can only cover some return patterns :(

- 

-         """

-         tested_func = builder_module.check_for_ans_error

- 

-         cases = [

-             {

-                 "args": [

-                     {

-                         "dark": {},

-                         "contacted": {}

-                     }, self.BUILDER_HOSTNAME

-                 ],

-                 "kwargs": {},

-                 "expected_return": None,

-                 "expected_exception": VmError

-             },

-             {

-                 "args": [

-                     {

-                         "dark": {self.BUILDER_HOSTNAME: ""},

-                         "contacted": {}

-                     }, self.BUILDER_HOSTNAME

-                 ],

-                 "kwargs": {},

-                 "expected_return": None,

-                 "expected_exception": VmError

-             },

-             {

-                 "args": [

-                     {

-                         "dark": {},

-                         "contacted": {self.BUILDER_HOSTNAME: {

-                             "rc": 0,

-                             "stdout": "stdout",

-                             "stderr": "stderr",

-                             "stdother": "stdother",

-                         }}

-                     }, self.BUILDER_HOSTNAME

-                 ],

-                 "kwargs": {},

-                 "expected_return": None,

-                 "expected_exception": None

-             },

-             {

-                 "args": [

-                     {

-                         "dark": {},

-                         "contacted": {self.BUILDER_HOSTNAME: {

-                             "rc": 1,

-                             "stdout": "stdout",

-                             "stderr": "stderr",

-                             "stdother": "stdother",

-                         }}

-                     }, self.BUILDER_HOSTNAME

-                 ],

-                 "kwargs": {},

-                 "expected_return": None,

-                 "expected_exception": AnsibleResponseError

-             },

-             {  # 5

-                 "args": [

-                     {

-                         "dark": {},

-                         "contacted": {self.BUILDER_HOSTNAME: {

-                             "rc": 1,

-                             "stdout": "stdout",

-                             "stderr": "stderr",

-                             "stdother": "stdother",

- 

-                         }}

-                     }, self.BUILDER_HOSTNAME

-                 ],

-                 "kwargs": {"success_codes": [0, 1]},

-                 "expected_return": None,

-                 "expected_exception": None,

-             },

-             {

-                 "args": [

-                     {

-                         "dark": {},

-                         "contacted": {self.BUILDER_HOSTNAME: {

-                             "rc": 2,

-                             "stdout": "stdout",

-                             "stderr": "stderr",

-                         }}

-                     }, self.BUILDER_HOSTNAME

-                 ],

-                 "kwargs": {"err_codes": [2, 3]},

-                 "expected_return": None,

-                 "expected_exception": AnsibleResponseError

-             },

-             {

-                 "args": [

-                     {

-                         "dark": {},

-                         "contacted": {self.BUILDER_HOSTNAME: {

-                             "failed": True,

-                             "stdout": "stdout",

-                             "stderr": "stderr",

-                             "stdother": "stdother",

-                         }}

-                     }, self.BUILDER_HOSTNAME

-                 ],

-                 "kwargs": {},

-                 "expected_return": None,

-                 "expected_exception": AnsibleResponseError

-             }

-         ]

-         # counter = 0

-         for case in cases:

-             if case["expected_exception"]:

-                 with pytest.raises(case["expected_exception"]):

-                     tested_func(*case["args"], **case["kwargs"])

-             else:

-                 result = tested_func(*case["args"], **case["kwargs"])

-                 assert result == case["expected_return"]

- 

-             # counter += 1

-             # print("\nCounter {} passed".format(counter))

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_get_ans_results(self):

-         result_obj = "RESULT_STRING"

-         results = {"dark": {self.BUILDER_HOSTNAME: result_obj}, "contacted": {}}

-         assert result_obj == builder_module.get_ans_results(results, self.BUILDER_HOSTNAME)

- 

-         results = {"contacted": {self.BUILDER_HOSTNAME: result_obj}, "dark": {}}

-         assert result_obj == builder_module.get_ans_results(results, self.BUILDER_HOSTNAME)

- 

-         results = {"contacted": {self.BUILDER_HOSTNAME: "wrong_obj"},

-                    "dark": {self.BUILDER_HOSTNAME: result_obj}}

-         assert result_obj == builder_module.get_ans_results(results, self.BUILDER_HOSTNAME)

- 

-         results = {"contacted": {}, "dark": {}}

-         assert {} == builder_module.get_ans_results(results, self.BUILDER_HOSTNAME)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_hostname_check(self, mc_socket):

-         mc_socket.gethostbyname.side_effect = socket.gaierror()

-         builder = self.get_test_builder()

-         for name in ["*", "256.0.0.1"]:

-             with pytest.raises(BuilderError):

-                 builder.checked = False

-                 builder.hostname = name

-                 builder.check()

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_missing_required_binaries(self, mc_socket):

-         builder = self.get_test_builder()

-         self.stage = 0

- 

-         def ans_run():

-             self.stage_ctx[self.stage]["conn"] = copy.deepcopy(builder.conn)

-             ret_map = {

-                 0: {"contacted": {self.BUILDER_HOSTNAME: {"rc": 1, }}},

-                 1: {"contacted": {self.BUILDER_HOSTNAME: {"rc": 0, }}}

-             }

-             self.stage += 1

-             return ret_map[self.stage - 1]

- 

-         builder.conn.run.side_effect = ans_run

-         with pytest.raises(BuilderError) as err:

-             builder.check()

- 

-         # import ipdb; ipdb.set_trace()

-         # pprint(self.stage_ctx)

-         assert "/bin/rpm -q mock rsync" in self.stage_ctx[0]["conn"].module_args

- 

-         assert "does not have mock or rsync installed" in err.value.msg

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_missing_mockchain_or_mock_config(self, mc_socket):

-         builder = self.get_test_builder()

- 

-         def ans_run():

-             self.stage_ctx[self.stage]["conn"] = copy.deepcopy(builder.conn)

-             ret_map = {

-                 0: {"contacted": {self.BUILDER_HOSTNAME: {"rc": 0, }}},

-                 1: {"contacted": {self.BUILDER_HOSTNAME: {"rc": 1, }}}

-             }

-             self.stage += 1

-             return ret_map[self.stage - 1]

- 

-         builder.conn.run.side_effect = ans_run

-         with pytest.raises(BuilderError) as err:

-             builder.check()

- 

-         # pprint(self.stage_ctx)

-         assert "/usr/bin/test -f /usr/bin/mockchain" in self.stage_ctx[1]["conn"].module_args

-         # assert "/usr/bin/test -f /etc/mock/{}.cfg".format(self.BUILDER_CHROOT) in \

-         #        self.stage_ctx[1]["conn"].module_args

- 

-         assert "missing mockchain binary" in err.value.msg

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_missing_mock_config(self, mc_socket):

-         builder = self.get_test_builder()

- 

-         ret_map = {

-             0: {"contacted": {self.BUILDER_HOSTNAME: {"rc": "0", }}},

-             1: {"contacted": {self.BUILDER_HOSTNAME: {"rc": "0", }}},

-             2: {"contacted": {self.BUILDER_HOSTNAME: {"failed": "fatal_2", }}}

-         }

- 

-         def ans_run():

-             self.stage_ctx[self.stage]["conn"] = copy.deepcopy(builder.conn)

-             self.stage += 1

-             return ret_map[self.stage - 1]

- 

-         builder.conn.run.side_effect = ans_run

-         with pytest.raises(BuilderError) as err:

-             builder.check()

- 

-         # pprint(self.stage_ctx)

-         assert "/usr/bin/test -f /usr/bin/mockchain" in self.stage_ctx[1]["conn"].module_args

-         assert "/usr/bin/test -f /etc/mock/{}.cfg".format(self.BUILDER_CHROOT) in \

-                self.stage_ctx[2]["conn"].module_args

- 

-         assert "missing mock config for chroot" in err.value.msg

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_tempdir_nop_when_provided(self):

-         builder = self.get_test_builder()

-         assert builder.tempdir == self.BUILDER_REMOTE_TMPDIR

-         assert not builder.conn.run.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_tempdir_failed_to_create(self):

-         builder = self.get_test_builder()

-         builder._remote_tempdir = None

- 

-         builder.conn.run.return_value = {"contacted": {

-             self.BUILDER_HOSTNAME: {"failed": "fatal_1", "stdout": None}}}

- 

-         with pytest.raises(BuilderError) as err:

-             x = builder.tempdir

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_tempdir_correct_creation(self):

-         builder = self.get_test_builder()

-         builder._remote_tempdir = None

- 

-         new_tmp_dir = "/tmp/new/"

- 

-         def ans_run():

-             self.stage_ctx[self.stage]["conn"] = copy.deepcopy(builder.conn)

-             ret_map = {

-                 0: {"contacted": {

-                     self.BUILDER_HOSTNAME: {"rc": 0, "stdout": new_tmp_dir}}},

-                 1: {"contacted": {

-                     self.BUILDER_HOSTNAME: {"rc": 0}}},

-             }

-             self.stage += 1

-             return ret_map[self.stage - 1]

- 

-         builder.conn.run.side_effect = ans_run

-         x = builder.tempdir

-         assert x == new_tmp_dir

-         assert "/bin/mktemp -d {0}".format(self.BUILDER_REMOTE_BASEDIR) in \

-                self.stage_ctx[0]["conn"].module_args

- 

-         assert "/bin/chmod 755 {}".format(new_tmp_dir) in \

-                self.stage_ctx[1]["conn"].module_args

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_tempdir_setter(self):

-         builder = self.get_test_builder()

-         builder._remote_tempdir = None

-         new_tmp_dir = "/tmp/new/"

-         builder.tempdir = new_tmp_dir

-         assert builder.tempdir == new_tmp_dir

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_modify_base_buildroot_malicious_vars(self):

-         builder = self.get_test_builder()

- 

-         for bad_pkg in [

-             "../'HOME-example.src.pkg",

-             # FIXME: i'm assuming that the following should also cause error

-             # "~HOM/E-example.src.pkg; rm -rf",

-             # "../%HOME-example.src.pkg",

-             # "../%HOME-example.src.pkg"

- 

-         ]:

-             with pytest.raises(BuilderError) as err:

-                 builder.buildroot_pkgs = bad_pkg

-                 builder.modify_mock_chroot_config()

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_modify_chroot_disable_networking(self):

-         storage = []

- 

-         def fake_run_ansible(self, cmd, *args, **kwargs):

-             storage.append(cmd)

- 

-         builder = self.get_test_builder()

-         builder.run_ansible_with_check = MethodType(fake_run_ansible, builder)

-         builder.root_conn.run.return_value = {

-             "contacted": {self.BUILDER_HOSTNAME: {"rc": 0, "stdout": None}},

-             "dark": {}

-         }

- 

-         self.job.enable_net = False

-         # net should be disabled

-         builder.modify_mock_chroot_config()

- 

-         expected = (

-             'dest=/etc/mock/fedora-20-i386.cfg '

-             'line="config_opts[\'use_host_resolv\'] = False" '

-             'regexp="^.*user_host_resolv.*$"')

-         assert any([expected in r for r in storage])

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_collect_build_packages(self):

-         builder = self.get_test_builder()

-         stdout = "stdout"

- 

-         builder.conn.run.return_value = {

-             "contacted": {self.BUILDER_HOSTNAME: {"rc": 0, "stdout": stdout}},

-             "dark": {}

-         }

-         builder.collect_built_packages()

-         expected = (

-             "cd {} && "

-             "for f in `ls *.rpm |grep -v \"src.rpm$\"`; do"

-             "   rpm -qp --qf \"%{{NAME}} %{{VERSION}}\n\" $f; "

-             "done".format(builder._get_remote_results_dir())

-         )

-         assert builder.conn.module_args == expected

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.builder.check_for_ans_error")

-     def test_run_ansible_with_check(self, mc_check_for_ans_errror):

-         builder = self.get_test_builder()

- 

-         cmd = "cmd"

-         module_name = "module_name"

-         as_root = True

- 

-         err_codes = [1, 3, 7, ]

-         success_codes = [0, 255]

- 

-         results = mock.MagicMock()

- 

-         err_results = mock.MagicMock()

- 

-         mc_check_for_ans_errror.return_value = (False, [])

-         builder._run_ansible = mock.MagicMock()

-         builder._run_ansible.return_value = results

- 

-         got_results = builder.run_ansible_with_check(

-             cmd, module_name, as_root, err_codes, success_codes)

- 

-         assert results == got_results

-         expected_call_run = mock.call(cmd, module_name, as_root)

-         assert expected_call_run == builder._run_ansible.call_args

-         expected_call_check = mock.call(results, builder.hostname,

-                                         err_codes, success_codes)

-         assert expected_call_check == mc_check_for_ans_errror.call_args

- 

-         mc_check_for_ans_errror.side_effect = AnsibleResponseError(msg="err message", **err_results)

- 

-         with pytest.raises(AnsibleCallError):

-             builder.run_ansible_with_check(

-                 cmd, module_name, as_root, err_codes, success_codes)

- 

- 

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.builder.check_for_ans_error")

-     def test_check_build_success(self, mc_check_for_ans_errror):

-         builder = self.get_test_builder()

- 

-         builder.check_build_success()

- 

-         expected_ans_args = (

-             "/usr/bin/test -f "

-             "/tmp/copr-backend-test-tmp/build/results/"

-             "{}/{}/success"

-         ).format(self.BUILDER_CHROOT, self.BUILDER_PKG_BASE)

-         assert expected_ans_args == builder.conn.module_args

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.builder.check_for_ans_error")

-     def test_check_build_exception(self, mc_check_for_ans_errror):

-         builder = self.get_test_builder()

- 

-         mc_check_for_ans_errror.side_effect = AnsibleResponseError(msg="err msg")

- 

-         with pytest.raises(BuilderError):

-             builder.check_build_success()

- 

-         expected_ans_args = (

-             "/usr/bin/test -f "

-             "/tmp/copr-backend-test-tmp/build/results/"

-             "{}/{}/success"

-         ).format(self.BUILDER_CHROOT, self.BUILDER_PKG_BASE)

-         assert expected_ans_args == builder.conn.module_args

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_get_mockchain_command(self):

-         builder = self.get_test_builder()

- 

-         builder.job.repos = [

-             "http://example.com/rhel7",

-             "http://example.com/fedora-20; rm -rf",

-             "http://example.com/fedora-$releasever",

-             "http://example.com/fedora-rawhide",

-         ]

-         result_cmd = builder.gen_mockchain_command()

-         expected = (

-             "/usr/bin/mockchain -r {chroot} -l /tmp/copr-backend-test-tmp/build/"

-             " -a http://example.com/rhel7 -a 'http://example.com/fedora-20; rm -rf' "

-             "-a 'http://example.com/fedora-$releasever' -a http://example.com/fedora-rawhide "

-             "-a {results_baseurl}/{owner}/{copr}/{chroot} -a {results_baseurl}/{owner}/{copr}/{chroot}/devel "

-             "-m '--define=copr_username {owner}' -m '--define=copr_projectname {copr}'"

-             " -m '--define=vendor Fedora Project COPR ({owner}/{copr})'"

-             " {build_target}").format(

-                 owner=self.job.project_owner,

-                 copr=self.job.project_name,

-                 chroot=self.job.chroot,

-                 build_target=self.BUILD_REMOTE_TARGET,

-                 results_baseurl=self.RESULT_DIR

-         )

-         assert result_cmd == expected

- 

-         # builder.chroot = "fedora-rawhide"

-         # builder.repos = [

-         #     "http://example.com/rhel7",

-         #     "http://example.com/fedora-20; rm -rf",

-         #     "http://example.com/fedora-$releasever",

-         #     "http://example.com/fedora-rawhide",

-         # ]

-         # builder.macros = {

-         #     "foo": "bar",

-         #     "foo; rm -rf": "bar",

-         #     "foo2": "bar; rm -rf"

-         # }

-         # result_cmd = builder.gen_mockchain_command(self.BUILDER_PKG)

-         # expected = (

-         #     "/usr/bin/mockchain -r fedora-rawhide -l /tmp/copr-backend-test-tmp/build/"

-         #     " -a http://example.com/rhel7 -a 'http://example.com/fedora-20; rm -rf' "

-         #     "-a http://example.com/fedora-rawhide -a http://example.com/fedora-rawhide "

-         #     "-m '--define=foo bar' -m '--define=foo; rm -rf bar' -m '--define=foo2 bar; rm -rf'"

-         #     " http://example.com/foovar-2.41.f21.src.rpm")

-         # assert result_cmd == expected

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.builder.time")

-     def test_run_command_and_wait_timeout(self, mc_time):

-         build_cmd = "foo bar"

-         builder = self.get_test_builder()

- 

-         mc_poller = mock.MagicMock()

-         mc_poller.poll.return_value = {"contacted": {}, "dark": {}}

-         builder.conn.run_async.return_value = None, mc_poller

-         builder.timeout = 50

- 

-         mc_time.return_value = None

- 

-         with pytest.raises(BuilderTimeOutError) as error:

-             builder.run_build_and_wait(build_cmd)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.builder.time")

-     def test_run_command_and_wait(self, mc_time):

-         build_cmd = "foo bar"

-         builder = self.get_test_builder()

- 

-         mc_poller = mock.MagicMock()

-         builder.conn.run_async.return_value = None, mc_poller

-         builder.timeout = 100

- 

-         expected_result = {"contacted": {self.BUILDER_HOSTNAME: True}, "dark": {}}

- 

-         def poll():

-             if self.stage < 2:

-                 return {"contacted": {}, "dark": {}}

-             else:

-                 return expected_result

- 

-         mc_poller.poll.side_effect = poll

- 

-         def incr_stage(*args, **kwargs):

-             self.stage += 1

- 

-         mc_time.sleep.side_effect = incr_stage

-         builder.run_build_and_wait(build_cmd)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.builder.Popen")

-     def test_download(self, mc_popen):

-         builder = self.get_test_builder()

- 

-         for ret_code, expected_success in [(0, True), (1, False), (23, False)]:

-             mc_cmd = mock.MagicMock()

-             mc_cmd.communicate.return_value = self.STDOUT, self.STDERR

-             mc_cmd.returncode = ret_code

-             mc_popen.return_value = mc_cmd

-             if expected_success:

-                 builder.download("target_dir")

-             else:

-                 with pytest.raises(BuilderError) as err:

-                     builder.download("target_dir")

-                 assert err.value.return_code == ret_code

-                 # assert err.value.stderr == self.STDERR

-                 # assert err.value.stdout == self.STDOUT

-             #

-             # expected_arg = (

-             #     "/usr/bin/rsync -avH -e 'ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no'"

-             #     " copr_builder@example.com:/tmp/copr-backend-test-tmp/build/results/fedora-20-i386/foovar-2.41.f21 "

-             #     "'/tmp/copr-backend-test'/ &> '/tmp/copr-backend-test'/build-00012345.rsync.log")

-             #

-             # assert mc_popen.call_args[0][0] == expected_arg

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.builder.Popen")

-     def test_download_popen_error(self, mc_popen):

-         builder = self.get_test_builder()

-         mc_popen.side_effect = IOError()

-         with pytest.raises(BuilderError):

-             builder.download(self.RESULT_DIR)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_build(self):

-         builder = self.get_test_builder()

-         builder.modify_mock_chroot_config = MagicMock()

-         builder.check_if_pkg_local_or_http = MagicMock()

-         builder.download_job_pkg_to_builder = MagicMock()

-         builder.download_job_pkg_to_builder.return_value = "foobar"

-         builder.check_if_pkg_local_or_http.return_value = self.BUILDER_PKG

- 

-         builder.run_build_and_wait = MagicMock()

-         successful_wait_result = {

-             "contacted": {self.BUILDER_HOSTNAME: {

-                 "rc": 0, "stdout": self.STDOUT, "stderr": self.STDERR

-             }},

-             "dark": {}

-         }

-         builder.run_build_and_wait.return_value = successful_wait_result

- 

-         builder.check_build_success = MagicMock()

-         builder.check_build_success.return_value = (self.STDERR, False, self.STDOUT)

- 

-         builder.collect_built_packages = MagicMock()

- 

-         stdout = builder.build()

-         assert stdout == self.STDOUT

- 

-         assert builder.modify_mock_chroot_config.called

-         assert builder.run_build_and_wait.called

-         assert builder.check_build_success.called

-         assert builder.collect_built_packages

- 

-         # test providing version / obsolete

-         builder.build()

- 

-         # test timeout handle

-         builder.run_build_and_wait.side_effect = BuilderTimeOutError("msg")

- 

-         with pytest.raises(BuilderError) as error:

-             builder.build()

- 

-         assert error.value.msg == "msg"

- 

-         # remove timeout

-         builder.run_build_and_wait.side_effect = None

-         builder.build()

- 

-         # error inside wait result

-         unsuccessful_wait_result = {

-             "contacted": {self.BUILDER_HOSTNAME: {

-                 "rc": 1, "stdout": self.STDOUT, "stderr": self.STDERR

-             }},

-             "dark": {}

-         }

-         builder.run_build_and_wait.return_value = unsuccessful_wait_result

-         with pytest.raises(BuilderError):

-             builder.build()

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_pre_process_repo_url(self):

-         builder = self.get_test_builder()

- 

-         cases = [

-             ("", "''"),

-             ("http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/fedora-$releasever-$basearch/",

-              "'http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/fedora-$releasever-$basearch/'"),

-             ("http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/$chroot/",

-              "http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/{}/".format(self.job.chroot)),

-             ("http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/$distname-$releasever-$basearch/",

-              "'http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/fedora-$releasever-$basearch/'"),

-             ("copr://foo/bar",

-              "{}/foo/bar/fedora-20-i386".format(self.opts.results_baseurl)),

-         ]

- 

-         for input_url, expected in cases:

-             assert builder.pre_process_repo_url(input_url) == expected

- 

-         self.job.chroot = "fedora-20-rawhide"

-         cases = [

-             ("http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/fedora-$releasever-$basearch/",

-              "'http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/fedora-$releasever-$basearch/'"),

-             ("http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/$chroot/",

-              "http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/{}/".format(self.job.chroot)),

-             ("http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/$distname-$releasever-$basearch/",

-              "'http://copr-be.c.fp.org/results/rhughes/f20-gnome-3-12/fedora-$releasever-$basearch/'"),

-         ]

-         for input_url, expected in cases:

-             expected = expected.replace("$releasever", "rawhide")

-             assert builder.pre_process_repo_url(input_url) == expected

- 

-         with mock.patch("{}.urlparse".format(MODULE_REF)) as handle:

-             handle.side_effect = IOError

-             for input_url, _ in cases:

-                 assert builder.pre_process_repo_url(input_url) is None

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_pubsub_build_interruption(self):

-         builder = self.get_test_builder()

-         builder.callback = MagicMock()

-         builder.ps = MagicMock()

-         for val in [None, {}, {"foo": "bar"}, {"type": "subscribe"}]:

-             builder.ps.get_message.return_value = val

-             builder.check_pubsub()

- 

-         builder.ps.get_message.return_value = {"type": "message", "data": ""}

-         with pytest.raises(VmError):

-             builder.check_pubsub()

- 

@@ -1,285 +0,0 @@

- # coding: utf-8

- import copy

- 

- from collections import defaultdict

- from munch import Munch

- from copr_backend.exceptions import MockRemoteError, CoprSignError, BuilderError

- 

- import tempfile

- import shutil

- import os

- 

- from unittest import mock, skip

- from unittest.mock import patch, MagicMock

- import pytest

- 

- from copr_backend.mockremote import MockRemote

- from copr_backend.job import BuildJob

- 

- 

- MODULE_REF = "copr_backend.mockremote"

- 

- STDOUT = "stdout"

- STDERR = "stderr"

- COPR_OWNER = "copr_owner"

- COPR_NAME = "copr_name"

- COPR_VENDOR = "vendor"

- 

- 

- class TestMockRemote(object):

- 

-     @pytest.yield_fixture

-     def f_mock_remote(self):

-         patcher = mock.patch("copr_backend.mockremote.Builder")

-         self.mc_builder = patcher.start()

-         self.mc_logger = MagicMock()

-         self.mr = MockRemote(self.HOST, self.JOB, opts=self.OPTS, logger=self.mc_logger)

-         self.mr.check()

-         yield

-         patcher.stop()

- 

-     def setup_method(self, method):

-         self.test_root_path = tempfile.mkdtemp()

-         self.CHROOT = "fedora-20-i386"

-         self.DESTDIR = os.path.join(self.test_root_path, COPR_OWNER, COPR_NAME)

-         self.DESTDIR_CHROOT = os.path.join(self.DESTDIR, self.CHROOT)

-         self.FRONT_URL = "htt://front.example.com"

-         self.BASE_URL = "http://example.com/results"

- 

-         self.PKG_NAME = "foobar"

-         self.PKG_VERSION = "1.2.3"

- 

-         self.HOST = "127.0.0.1"

-         self.SRC_PKG_URL = "http://example.com/{}-{}.src.rpm".format(self.PKG_NAME, self.PKG_VERSION)

- 

-         self.GIT_HASH = "1234r"

-         self.GIT_BRANCH = "f20"

-         self.GIT_REPO = "foo/bar/xyz"

- 

-         self.JOB = BuildJob({

-             "project_owner": COPR_OWNER,

-             "project_name": COPR_NAME,

-             "project_dirname": COPR_NAME,

-             "pkgs": self.SRC_PKG_URL,

-             "repos": "",

-             "build_id": 12345,

-             "chroot": self.CHROOT,

- 

-             "git_repo": self.GIT_REPO,

-             "git_hash": self.GIT_HASH,

-             "git_branch": self.GIT_BRANCH,

- 

-             "package_name": self.PKG_NAME,

-             "package_version": self.PKG_VERSION

-         }, Munch({

-             "timeout": 1800,

-             "destdir": self.test_root_path,

-             "results_baseurl": "/tmp/",

-         }))

- 

-         self.OPTS = Munch({

-             "do_sign": False,

-             "results_baseurl": self.BASE_URL,

-             "frontend_base_url": self.FRONT_URL,

-         })

- 

-     def teardown_method(self, method):

-         shutil.rmtree(self.test_root_path)

- 

-     def test_dummy(self, f_mock_remote):

-         pass

- 

-     def test_no_job_chroot(self, f_mock_remote, capsys):

-         job_2 = copy.deepcopy(self.JOB)

-         job_2.chroot = None

-         mr_2 = MockRemote(self.HOST, job_2, opts=self.OPTS, logger=self.mc_logger)

-         with pytest.raises(MockRemoteError):

-             mr_2.check()

- 

-         out, err = capsys.readouterr()

- 

-     @mock.patch("copr_backend.mockremote.get_pubkey")

-     def test_add_pubkey(self, mc_get_pubkey, f_mock_remote):

-         self.mr.add_pubkey()

-         assert mc_get_pubkey.called

-         expected_path = os.path.join(self.DESTDIR, "pubkey.gpg")

-         assert mc_get_pubkey.call_args == mock.call(

-             COPR_OWNER, COPR_NAME, expected_path)

- 

-     @mock.patch("copr_backend.mockremote.get_pubkey")

-     def test_add_pubkey_on_exception(self, mc_get_pubkey, f_mock_remote):

-         mc_get_pubkey.side_effect = CoprSignError("foobar")

-         # doesn't raise an error

-         self.mr.add_pubkey()

- 

-     @mock.patch("copr_backend.mockremote.sign_rpms_in_dir")

-     def test_sign_built_packages(self, mc_sign_rpms_in_dir, f_mock_remote):

-         self.mr.sign_built_packages()

-         assert mc_sign_rpms_in_dir.called

- 

-     @mock.patch("copr_backend.mockremote.sign_rpms_in_dir")

-     def test_sign_built_packages_exception(self, mc_sign_rpms_in_dir, f_mock_remote):

-         mc_sign_rpms_in_dir.side_effect = IOError()

-         # doesn't raise an error

-         self.mr.sign_built_packages()

- 

-     @mock.patch("copr_backend.mockremote.sign_rpms_in_dir")

-     def test_sign_built_packages_exception_reraise(self, mc_sign_rpms_in_dir, f_mock_remote):

-         mc_sign_rpms_in_dir.side_effect = MockRemoteError("foobar")

-         with pytest.raises(MockRemoteError):

-             self.mr.sign_built_packages()

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.createrepo")

-     def test_do_createrepo(self, mc_createrepo, f_mock_remote):

-         mc_createrepo.return_value = ("", "", "")

-         self.mr.do_createrepo()

-         assert mc_createrepo.called

-         expected_call = mock.call(

-             path=os.path.join(self.DESTDIR, self.CHROOT),

-             front_url=self.FRONT_URL,

-             base_url=u"/".join([self.BASE_URL, COPR_OWNER, COPR_NAME, self.CHROOT]),

-             username=COPR_OWNER,

-             projectname=COPR_NAME,

-         )

-         assert mc_createrepo.call_args == expected_call

- 

-     @skip("Fixme or remove, test doesn't work.")

-     @mock.patch("copr_backend.mockremote.createrepo")

-     def test_do_createrepo_on_error(self, mc_createrepo, f_mock_remote):

-         err_msg = "error occurred"

-         mc_createrepo.return_value = ("", "", err_msg)

-         # doesn't raise an error

-         self.mr.do_createrepo()

- 

-     def test_on_success_build(self, f_mock_remote):

-         self.mr.sign_built_packages = MagicMock()

-         self.mr.do_createrepo = MagicMock()

- 

-         self.mr.opts.do_sign = False

-         self.mr.on_success_build()

- 

-         assert not self.mr.sign_built_packages.called

-         assert self.mr.do_createrepo.called

- 

-         self.mr.do_createrepo.reset()

- 

-         self.mr.opts.do_sign = True

-         self.mr.on_success_build()

- 

-         assert self.mr.sign_built_packages.called

-         assert self.mr.do_createrepo.called

- 

-     def test_prepare_build_dir_erase_fail_file(self, f_mock_remote):

-         target_dir = self.mr.job.results_dir

-         os.makedirs(target_dir)

-         fail_path = os.path.join(target_dir, "fail")

-         with open(fail_path, "w") as handle:

-             handle.write("1")

-         assert os.path.exists(fail_path)

- 

-         self.mr.prepare_build_dir()

-         assert os.path.exists(fail_path) is False

- 

-     def test_prepare_build_dir_erase_success_file(self, f_mock_remote):

-         target_dir = self.mr.job.results_dir

-         os.makedirs(target_dir)

-         fail_path = os.path.join(target_dir, "success")

-         with open(fail_path, "w") as handle:

-             handle.write("1")

-         assert os.path.exists(fail_path)

- 

-         self.mr.prepare_build_dir()

- 

-         assert os.path.exists(fail_path) is False

- 

-     def test_prepare_build_dir_creates_dirs(self, f_mock_remote):

-         self.mr.prepare_build_dir()

-         assert os.path.exists(self.mr.job.results_dir)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_build_pkg_and_process_results(self, f_mock_remote):

-         self.mr.on_success_build = MagicMock()

-         self.mr.mark_dir_with_build_id = MagicMock()

- 

-         build_details = MagicMock()

-         self.mr.builder.build.return_value = STDOUT

-         self.mr.builder.collect_built_packages.return_value = "foo bar"

- 

-         result = self.mr.build_pkg_and_process_results()

- 

-         assert result["built_packages"] == "foo bar"

- 

-         assert self.mr.builder.build.called

-         assert self.mr.builder.download.called

-         assert self.mr.mark_dir_with_build_id.called

-         assert self.mr.on_success_build.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_build_pkg_and_process_results_error_on_download(self, f_mock_remote):

-         self.mr.builder.build.return_value = ({}, STDOUT)

-         self.mr.builder.download.side_effect = BuilderError(msg="STDERR")

- 

-         self.mr.mark_dir_with_build_id = MagicMock()

-         self.mr.on_success_build = MagicMock()

-         with pytest.raises(MockRemoteError):

-             self.mr.build_pkg_and_process_results()

- 

-         assert not self.mr.on_success_build.called

-         assert self.mr.mark_dir_with_build_id.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_build_pkg_and_process_results_error_on_build(self, f_mock_remote):

-         # self.mr.builder.build.return_value = ({}, STDOUT)

-         self.mr.builder.build.side_effect = BuilderError(msg="STDERR")

-         # self.mr.builder.download.return_value = BuilderError(msg="STDERR")

- 

-         self.mr.mark_dir_with_build_id = MagicMock()

-         self.mr.on_success_build = MagicMock()

-         with pytest.raises(MockRemoteError):

-             self.mr.build_pkg_and_process_results()

- 

-         assert not self.mr.on_success_build.called

-         assert self.mr.mark_dir_with_build_id.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_mark_dir_with_build_id(self, f_mock_remote):

-         # TODO: create real test

-         target_dir = self.mr.job.results_dir

-         os.makedirs(target_dir)

- 

-         info_file_path = os.path.join(target_dir, "build.info")

-         assert not os.path.exists(info_file_path)

-         self.mr.mark_dir_with_build_id()

- 

-         assert os.path.exists(info_file_path)

-         with open(info_file_path) as handle:

-             assert str(self.JOB.build_id) in handle.read()

- 

-         with mock.patch("__builtin__.open".format(MODULE_REF)) as mc_open:

-             mc_open.side_effect = IOError()

-             # do not raise an error

-             self.mr.mark_dir_with_build_id()

- 

-     # def test_add_log_symlinks(self, f_mock_remote):

-     #     base = os.path.join(self.DESTDIR_CHROOT,

-     #                         "{}-{}".format(self.PKG_NAME, self.PKG_VERSION))

-     #     os.makedirs(base)

-     #

-     #     names = ["build.log", "root.log"]

-     #     for name in names:

-     #         open(os.path.join(base, name + ".gz"), "a").close()

-     #         assert not os.path.exists(os.path.join(base, name))

-     #

-     #     dir_name = "i_am_a_dir.log"

-     #     os.mkdir(os.path.join(base, dir_name + ".gz"))

-     #     assert not os.path.exists(os.path.join(base, dir_name))

-     #

-     #     self.mr.add_log_symlinks()

-     #     assert not os.path.exists(os.path.join(base, dir_name))

-     #     for name in names:

-     #         assert os.path.exists(os.path.join(base, name))

- 

- 

- 

- 

@@ -0,0 +1,860 @@

+ # coding: utf-8

+ 

+ """

+ Test the BuildBackgroundWorker from background_worker_build.py

+ """

+ 

+ import copy

+ import logging

+ import glob

+ import json

+ import os

+ import shutil

+ import subprocess

+ import time

+ import tempfile

+ from unittest import mock

+ 

+ from munch import Munch

+ import pytest

+ 

+ from copr_backend.constants import LOG_REDIS_FIFO

+ from copr_backend.background_worker_build import (

+     BuildBackgroundWorker, MESSAGES, BackendError, _average_step,

+ )

+ from copr_backend.job import BuildJob

+ from copr_backend.exceptions import CoprSignError

+ from copr_backend.vm_alloc import ResallocHost, RemoteHostAllocationTerminated

+ from copr_backend.background_worker_build import COMMANDS, MIN_BUILDER_VERSION

+ from copr_backend.sshcmd import SSHConnectionError

+ from copr_backend.exceptions import CoprBackendSrpmError

+ 

+ import testlib

+ from testlib.repodata import load_primary_xml

+ 

+ # pylint: disable=redefined-outer-name,protected-access

+ 

+ COMMON_MSGS = {

+     "not finished": "Switching not-finished job state to 'failed'",

+ }

+ 

+ def _patch_bw_object(obj, *args, **kwargs):

+     return mock.patch("copr_backend.background_worker.{}".format(obj),

+                       *args, **kwargs)

+ 

+ def _patch_bwbuild_object(obj, *args, **kwargs):

+     return mock.patch("copr_backend.background_worker_build.{}".format(obj),

+                       *args, **kwargs)

+ 

+ def _get_rpm_job_object_dict(updated=None):

+     job = copy.deepcopy(testlib.VALID_RPM_JOB)

+     if updated:

+         job.update(updated)

+     return job

+ 

+ def _get_rpm_job_object(opts, updated=None):

+     job = _get_rpm_job_object_dict(updated)

+     return BuildJob(job, opts)

+ 

+ def _get_rpm_job(updated=None):

+     job = _get_rpm_job_object_dict(updated)

+     response = Munch()

+     response.status_code = 200

+     response.json = lambda: job

+     return response

+ 

+ def _get_srpm_job(updated=None):

+     job = copy.deepcopy(testlib.VALID_SRPM_JOB)

+     if updated:

+         job.update(updated)

+     response = Munch()

+     response.status_code = 200

+     response.json = lambda: job

+     return response

+ 

+ def _reset_build_worker():

+     worker = BuildBackgroundWorker()

+     # Don't waste time with mocking.  We don't want to log anywhere, and we want

+     # to let BuildBackgroundWorker adjust the handlers.

+     worker.log.handlers = []

+     return worker

+ 

+ def _fake_host():

+     host = ResallocHost()

+     host._is_ready = True

+     host.hostname = "1.2.3.4"

+     host.ticket = mock.MagicMock()

+     host.ticket.id = 10

+     host.release = mock.MagicMock()

+     return host

+ 

+ @pytest.fixture

+ def f_build_something():

+     """

+     Prepare worker for RPM or SRPM build

+     """

+     config = Munch()

+     config.workdir = tempfile.mkdtemp(prefix="build-worker-tests-")

+     config.be_config_file = testlib.minimal_be_config(config.workdir)

+ 

+     patchers = [_patch_bw_object("FrontendClient")]

+     config.fe_client = patchers[-1].start()

+ 

+     patchers += [_patch_bwbuild_object("MessageSender")]

+     patchers[-1].start()

+ 

+     config.fe_client_patcher = _patch_bw_object("FrontendClient")

+     config.fe_client = config.fe_client_patcher.start()

+ 

+     config.worker_id = "fake_worker_id_" + str(time.time())

+ 

+     patchers.append(

+         mock.patch("copr_backend.background_worker.sys.argv",

+                    ["test-build", "--build-id", "848963",

+                     "--worker-id", config.worker_id, "--silent",

+                     "--backend-config", config.be_config_file,

+                     "--chroot", "fedora-30-x86_64"]),

+     )

+     patchers[-1].start()

+ 

+     patchers.append(mock.patch.dict(os.environ, {

+         "COPR_BE_CONFIG": config.be_config_file,

+         "COPR_TESTSUITE_LOCKPATH": config.workdir,

+     }))

+     patchers[-1].start()

+ 

+     config.bw = _reset_build_worker()

+     config.bw.redis_set_worker_flag("allocated", "true")

+ 

+     # Don't waste time with mocking.  We don't want to log anywhere, and we want

+     # to let BuildBackgroundWorker adjust the handlers.

+     config.bw.log.handlers = []

+ 

+     patcher = _patch_bwbuild_object("ResallocHostFactory")

+     patchers.append(patcher)

+     rhf = patcher.start()

+     config.host = host = _fake_host()

+     rhf.return_value.get_host.return_value = host

+     config.resalloc_host_factory = rhf

+ 

+     config.ssh = testlib.FakeSSHConnection(user="remoteuser", host=host.hostname)

+     ssh_patcher = _patch_bwbuild_object("SSHConnection")

+     ssh_class = ssh_patcher.start()

+     ssh_class.return_value = config.ssh

+     patchers.append(ssh_patcher)

+ 

+     yield config

+     for patcher in patchers:

+         patcher.stop()

+     shutil.rmtree(config.workdir)

+ 

+     # Clear the adjusted handlers.

+     config.bw.log.handlers = []

+ 

+ @pytest.fixture

+ def f_build_rpm_case_no_repodata(f_build_something):

+     """

+     Configure the situation when 'copr-backend-process-build' is requested

+     to build RPM.

+     """

+     config = f_build_something

+     config.fe_client.return_value.get.return_value = _get_rpm_job()

+     yield config

+ 

+ @pytest.fixture

+ def f_build_srpm(f_build_something):

+     """

+     Configure the situation when 'copr-backend-process-build' is requested

+     to build RPM.

+     """

+     config = f_build_something

+     config.fe_client.return_value.get.return_value = _get_srpm_job()

+     config.ssh.set_command(

+         "copr-rpmbuild --verbose --drop-resultdir --srpm --build-id 855954 "

+         "--detached",

+         0, "666", "",

+     )

+ 

+     patcher = mock.patch(

+         "copr_backend.background_worker.sys.argv",

+         ["test-build", "--build-id", "848963",

+          "--worker-id", config.worker_id, "--silent",

+          "--backend-config", config.be_config_file,

+          "--chroot", "srpm-builds"],

+     )

+     patcher.start()

+     config.bw = _reset_build_worker()

+     yield config

+     patcher.stop()

+ 

+ def _create_repodata(in_dir):

+     repodata = os.path.join(in_dir, "repodata")

+     os.makedirs(repodata)

+     repomd = os.path.join(repodata, "repomd.xml")

+     with open(repomd, "w"):

+         pass

+ 

+ 

+ def _create_job_repodata(job):

+     _create_repodata(job.chroot_dir)

+ 

+ 

+ @pytest.fixture

+ def f_build_rpm_case(f_build_rpm_case_no_repodata):

+     """

+     Prepare everything, so the build can succeed.

+     """

+     config = f_build_rpm_case_no_repodata

+     chroot = os.path.join(f_build_rpm_case_no_repodata.workdir, "results",

+                           "@copr",

+                           "TEST1575431880356948981Project10",

+                           "fedora-30-x86_64")

+     _create_repodata(chroot)

+ 

+     config.ssh.set_command(

+         "copr-rpmbuild --verbose --drop-resultdir --build-id 848963 "

+         "--chroot fedora-30-x86_64 --detached",

+         0, "666", "",

+     )

+     yield f_build_rpm_case_no_repodata

+ 

+ @pytest.fixture

+ def f_build_rpm_sign_on(f_build_rpm_case):

+     """

+     f_build_rpm_case with enabled GPG signing ON

+     """

+     config = f_build_rpm_case

+     with open(config.be_config_file, "a+") as fdconfig:

+         fdconfig.write("do_sign=true\n")

+         fdconfig.write("keygen_host=keygen.example.com\n")

+     config.bw = _reset_build_worker()

+     return config

+ 

+ @_patch_bwbuild_object("time")

+ def test_waiting_for_repo_fail(mc_time, f_build_rpm_case_no_repodata, caplog):

+     """ check that worker loops in _wait_for_repo """

+     worker = f_build_rpm_case_no_repodata.bw

+     mc_time.time.side_effect = [1, 2, 3, 4, 5, 6, 120, 121]

+     worker.process()

+     expected = [

+         (logging.ERROR, str(BackendError(MESSAGES["give_up_repo"]))),

+         (logging.INFO, MESSAGES["repo_waiting"]),

+     ]

+     for exp in expected:

+         assert exp in [(r[1], r[2]) for r in caplog.record_tuples]

+ 

+ @_patch_bwbuild_object("time")

+ def test_waiting_for_repo_success(mc_time, f_build_rpm_case_no_repodata, caplog):

+     """ check that worker loops in _wait_for_repo """

+     worker = f_build_rpm_case_no_repodata.bw

+ 

+     # on the 6th call to time(), create the repodata

+     mc_time.time.side_effect = testlib.TimeSequenceSideEffect(

+         [1, 2, 3, 4, 5, 6, 120],

+         {6: lambda: _create_job_repodata(worker.job)}

+     )

+ 

+     # shutdown ASAP after _wait_for_repo() call

+     def raise_exc():

+         raise Exception("duh")

+     worker._alloc_host = mock.MagicMock()

+     worker._alloc_host.side_effect = raise_exc

+     worker.process()

+ 

+     # _wait_for_repo() succeeded, and we continued to _alloc_host()

+     assert len(worker._alloc_host.call_args_list) == 1

+ 

+     assert (logging.INFO, MESSAGES["repo_waiting"]) \

+         in [(r[1], r[2]) for r in caplog.record_tuples]

+ 

+ def test_full_rpm_build_no_sign(f_build_rpm_case, caplog):

+     """

+     Go through the whole (successful) build of a binary RPM

+     """

+     worker = f_build_rpm_case.bw

+     worker.process()

+ 

+     results = worker.job.results_dir

+     assert os.path.exists(os.path.join(results, "builder-live.log.gz"))

+     assert os.path.exists(os.path.join(results, "backend.log.gz"))

+ 

+     found_success_log_entry = False

+     for record in caplog.record_tuples:

+         _, level, msg = record

+         assert level < logging.ERROR

+         if "Finished build: id=848963 failed=False" in msg:

+             found_success_log_entry = True

+     assert found_success_log_entry

+ 

+     repodata = load_primary_xml(os.path.join(results, "..", "repodata"))

+     assert repodata["names"] == {"example"}

+     assert repodata["packages"]["example"]["href"] == \

+         "00848963-example/example-1.0.14-1.fc30.x86_64.rpm"

+ 

+     assert worker.job.built_packages == "example 1.0.14"

+     assert_messages_sent(["build.start", "chroot.start", "build.end"], worker.sender)

+ 

+ def test_prev_build_backup(f_build_rpm_case):

+     worker = f_build_rpm_case.bw

+     worker.process()

+     worker.process()

+     prev_results = os.path.join(worker.job.results_dir, "prev_build_backup")

+     assert glob.glob(os.path.join(prev_results, '*.rpm')) == []

+     assert os.path.exists(os.path.join(prev_results, "builder-live.log.gz"))

+     assert os.path.exists(os.path.join(prev_results, "backend.log.gz"))

+     rsync_patt = "*{}.rsync.log".format(worker.job.build_id)

+     assert len(glob.glob(os.path.join(prev_results, rsync_patt))) == 1

+ 

+ def test_full_srpm_build(f_build_srpm):

+     worker = f_build_srpm.bw

+     worker.process()

+     assert worker.job.pkg_name == "example"

+ 

+     # TODO: fix this is ugly pkg_version testament

+     assert worker.job.pkg_version is None

+     assert worker.job.__dict__["pkg_version"] == "1.0.14-1.fc30"

+ 

+     assert worker.job.srpm_url == (

+         "https://example.com/results/@copr/PROJECT_2/srpm-builds/"

+         "00855954/example-1.0.14-1.fc30.src.rpm")

+ 

+ @mock.patch("copr_backend.sign.SIGN_BINARY", "tests/fake-bin-sign")

+ @mock.patch("copr_backend.sign._sign_one")

+ def test_build_and_sign(mc_sign_one, f_build_rpm_sign_on, caplog):

+     config = f_build_rpm_sign_on

+     worker = config.bw

+     worker.process()

+     pub_key = os.path.join(worker.job.destdir, "pubkey.gpg")

+     with open(pub_key, "r") as pub:

+         content = pub.readlines()

+         assert content == ["fake pub key content\n"]

+ 

+     mail = "@copr#TEST1575431880356948981Project10@copr.fedorahosted.org"

+     rpm = os.path.join(worker.job.results_dir,

+                        "example-1.0.14-1.fc30.x86_64.rpm")

+     srpm = os.path.join(worker.job.results_dir,

+                         "example-1.0.14-1.fc30.src.rpm")

+     assert mc_sign_one.call_args_list == [mock.call(rpm, mail), mock.call(srpm, mail)]

+     for record in caplog.record_tuples:

+         _, level, _ = record

+         assert level <= logging.INFO

+ 

+ @mock.patch("copr_backend.sign.SIGN_BINARY", "tests/fake-bin-sign")

+ @mock.patch("copr_backend.sign._sign_one")

+ @_patch_bwbuild_object("sign_rpms_in_dir")

+ def test_sign_built_packages_exception(mc_sign_rpms, mc_sign_one,

+                                        f_build_rpm_sign_on, caplog):

+     _side_effect = mc_sign_one

+     mc_sign_rpms.side_effect = CoprSignError("test")

+     config = f_build_rpm_sign_on

+     worker = config.bw

+     worker.process()

+     messages = [

+         (logging.ERROR, "Copr GPG signing problems: test"),

+     ]

+     found_fail = False

+     for msg in messages:

+         assert msg in [(r[1], r[2]) for r in caplog.record_tuples]

+     for msg in caplog.record_tuples:

+         _, _, text = msg

+         if "Finished build: id=848963 failed=True" in text:

+             found_fail = True

+     assert found_fail

+ 

+ def _get_log_content(job, log="backend.log.gz"):

+     logfile = os.path.join(job.results_dir, log)

+     cmd = ["gunzip", "-c", logfile]

+     return subprocess.check_output(cmd).decode("utf-8")

+ 

+ def test_unexpected_exception(f_build_rpm_case, caplog):

+     config = f_build_rpm_case

+     worker = config.bw

+     def _raise():

+         raise IOError("blah")

+ 

+     worker._check_vm = _raise

+     worker.process()

+ 

+     redis = worker._redis

+     log_entry = None

+ 

+     # check that the traceback is logged to worker.log

+     while True:

+         log_entry = redis.rpop(LOG_REDIS_FIFO)

+         if not log_entry:

+             break

+         if "Traceback" in log_entry:

+             break

+ 

+     log_dict = json.loads(log_entry)

+     msg = "Unexpected exception\nTraceback (most recent call last)"

+     assert msg in log_dict["msg"]

+ 

+     content = _get_log_content(worker.job)

+     assert "Traceback" not in content

+ 

+     found_line = None

+     for line in content.splitlines():

+         if "Unexpected exception" in line:

+             found_line = line

+             break

+ 

+     # check that the shortened variant is in log

+     assert "/test_background_worker_build.py:" in found_line

+ 

+ def test_build_info_file_failure(f_build_rpm_case):

+     config = f_build_rpm_case

+     worker = config.bw

+     worker.job = _get_rpm_job_object(worker.opts)

+     os.makedirs(worker.job.results_dir)

+     info_file = os.path.join(worker.job.results_dir, "build.info")

+     worker.host = Munch()

+     worker.host.hostname = "0.0.0.0"

+     worker._fill_build_info_file()

+     with open(info_file, "r") as info_fd:

+         content = info_fd.readlines()

+         # TODO: fix missing newline

+         assert content == ['build_id=848963\n', 'builder_ip=0.0.0.0']

+     # make it "non-writable"

+     os.unlink(info_file)

+     os.mkdir(info_file)

+     with pytest.raises(BackendError) as err:

+         worker._fill_build_info_file()

+     assert "Backend process error: Can't write to " in str(err.value)

+     assert "00848963-example/build.info'" in str(err.value)

+ 

+ def test_invalid_job_info(f_build_rpm_case, caplog):

+     config = f_build_rpm_case

+     worker = config.bw

+     get = config.fe_client.return_value.get.return_value

+     job = get.json()

+     del job["chroot"]

+     worker.process()

+     assert_logs_exist([

+         "Backend process error: Frontend job doesn't provide chroot",

+         COMMON_MSGS["not finished"],

+     ], caplog)

+     assert_logs_dont_exist([

+         "took None",

+     ], caplog)

+ 

+ @mock.patch("copr_backend.vm_alloc.time.sleep", mock.MagicMock())

+ @_patch_bwbuild_object("CANCEL_CHECK_PERIOD", 0.5)

+ @mock.patch("copr_backend.sign.SIGN_BINARY", "tests/fake-bin-sign")

+ def test_cancel_build_on_vm_allocation(f_build_rpm_sign_on, caplog):

+     config = f_build_rpm_sign_on

+     worker = config.bw

+ 

+     # let it think it is started by "worker manager"

+     class _CheckReady:

+         attempt = 0

+         def __call__(self):

+             # process() configures logging, so drop the loggers to avoid

+             # an ugly test output

+             self.attempt += 1

+             if self.attempt > 3:

+                 # Deliver cancel request to checker thread (this is

+                 # delivered by WorkerManager normally).

+                 worker.redis_set_worker_flag("cancel_request", 1)

+ 

+             # When the "cancel_request" is processed, we get the "canceling"

+             # response.  Resalloc client would normally recognize that the

+             # ticket is closed, and raised RemoteHostAllocationTerminated.

+             if worker.redis_get_worker_flag("canceling"):

+                 raise RemoteHostAllocationTerminated

+             return False

+ 

+     config.host._is_ready = False

+     config.host.check_ready = mock.MagicMock()

+     config.host.check_ready.side_effect = _CheckReady()

+ 

+     found_records = {}

+     def _find_records(record_msg):

+         msgs = [

+             "Build was canceled", # cancel handled

+             COMMON_MSGS["not finished"],

+             "Worker failed build", # needs to finish

+             "Unable to compress file", # there's no builder-live.log yet

+         ]

+         for msg in msgs:

+             if not msg in found_records:

+                 found_records[msg] = False

+             if msg in record_msg:

+                 found_records[msg] = True

+ 

+     worker.process()

+     for record in caplog.record_tuples:

+         _, _, msg = record

+         _find_records(msg)

+     for key, value in found_records.items():

+         assert (key, value) == (key, True)

+     assert worker.job.status == 0  # failure

+ 

+ class _CancelFunction():

+     def __init__(self, worker):

+         self.worker = worker

+     def __call__(self):

+         # request cancelation

+         self.worker.redis_set_worker_flag("cancel_request", 1)

+         # and do something till _cancel_vm_allocation() doesn't let us know

+         while self.worker.redis_get_worker_flag("canceling") is None:

+             time.sleep(0.25)

+ 

+ @_patch_bwbuild_object("CANCEL_CHECK_PERIOD", 0.5)

+ @mock.patch("copr_backend.sign.SIGN_BINARY", "tests/fake-bin-sign")

+ def test_cancel_build_on_tail_log_no_ssh(f_build_rpm_sign_on, caplog):

+     config = f_build_rpm_sign_on

+     worker = config.bw

+ 

+     config.ssh.set_command(

+         "copr-rpmbuild-log",

+         0, "canceled stdout\n", "canceled stderr\n",

+         _CancelFunction(worker),

+     )

+     worker.process()

+     exp_msgs = {

+         "Can't ssh to cancel build.",

+         "Build was canceled",

+         COMMON_MSGS["not finished"],

+         "Worker failed build",

+     }

+     found_messages = set()

+ 

+     for record in caplog.record_tuples:

+         _, _, msg = record

+         for exp_msg in exp_msgs:

+             if exp_msg in msg:

+                 found_messages.add(exp_msg)

+     assert exp_msgs == found_messages

+     log = _get_log_content(worker.job, "builder-live.log.gz")

+     assert "canceled stdout" in log

+ 

+ @_patch_bwbuild_object("CANCEL_CHECK_PERIOD", 0.5)

+ @mock.patch("copr_backend.sign.SIGN_BINARY", "tests/fake-bin-sign")

+ def test_build_retry(f_build_rpm_sign_on):

+     config = f_build_rpm_sign_on

+     worker = config.bw

+     class _SideEffect():

+         counter = 0

+         def __call__(self):

+             self.counter += 1

+             if self.counter < 2:

+                 return (1, "out", "err")

+             if self.counter < 3:

+                 return (0, "0.38", "")

+             return (0, MIN_BUILDER_VERSION, "")

+ 

+     config.ssh.set_command(

+         COMMANDS["rpm_q_builder"],

+         1, "err stdout\n", "err stderr\n",

+         return_action=_SideEffect())

+ 

+     worker.process()

+     log = _get_log_content(worker.job)

+     find_msgs = {

+         MESSAGES["copr_rpmbuild_missing"].format("err"),

+         "Minimum version for builder is " + MIN_BUILDER_VERSION,

+         "Allocating ssh connection to builder",

+         "Finished build: id=848963 failed=False",

+     }

+     found_msgs = set()

+     for line in log.splitlines():

+         for find in find_msgs:

+             if find in line:

+                 found_msgs.add(find)

+     assert find_msgs == found_msgs

+     assert _get_log_content(worker.job, "builder-live.log.gz").splitlines() == \

+         ["build log stdout", "build log stderr"]

+ 

+ def assert_messages_sent(topics, sender):

+     """ check msg bus calls """

+     assert len(topics) == len(sender.announce.call_args_list)

+     for topic in topics:

+         found = False

+         for call in sender.announce.call_args_list:

+             if topic == call[0][0]:

+                 found = True

+         assert (found, topic) == (True, topic)

+ 

+ def assert_logs_exist(messages, caplog):

+     """

+     Search through caplog entries for log records having all the messages in

+     ``messages`` list.

+     """

+     search_for = set(messages)

+     found = set()

+     for record in caplog.record_tuples:

+         _, _, msg = record

+         for search in search_for:

+             if search in msg:

+                 found.add(search)

+     assert found == search_for

+ 

+ def assert_logs_dont_exist(messages, caplog):

+     """

+     Search through caplog entries for log records having all the messages in

+     ``messages`` list.

+     """

+     search_for = set(messages)

+     found = set()

+     for record in caplog.record_tuples:

+         _, _, msg = record

+         for search in search_for:

+             if search in msg:

+                 found.add(search)

+     assert found == set({})

+ 

+ def test_fe_disallowed_start(f_build_rpm_sign_on, caplog):

Can we use @pytest.mark.usefixtures here?
Also, I prefer to have test functions inside of a class because then it is easier to group them into smaller chunks if there are some common themes. But it probably doesn't matter here.

The fixture is not only about side effects but is actually used.. I should probably originally start with the class as you'd preffer, or not start using with the fixtures (this is sort of invalid use-case) but the more I wrote, the more lazy I was to restart with writing the test file. :-)

+     config = f_build_rpm_sign_on

+     worker = config.bw

+     config.fe_client.return_value.starting_build.return_value = False

+     worker.process()

+     assert any(["Frontend forbade to start" in r[2] for r in caplog.record_tuples])

+     assert any(["Worker failed build" in r[2] for r in caplog.record_tuples])

+ 

+ def test_fe_failed_start(f_build_rpm_sign_on, caplog):

+     config = f_build_rpm_sign_on

+     worker = config.bw

+     job = _get_rpm_job()

+     job.status_code = 403

+     config.fe_client.return_value.get.return_value = job

+     worker.process()

+     assert_logs_exist([

+         "Failed to download build info, apache code 403",

+         "Backend process error: Failed to get the build task"

+         " get-build-task/848963-fedora-30-x86_64",

+         "No job object from Frontend",

+     ], caplog)

+     # check that worker manager is notified

+     assert worker.redis_get_worker_flag("status") == "done"

+ 

+ @_patch_bwbuild_object("CANCEL_CHECK_PERIOD", 0.5)

+ @mock.patch("copr_backend.sign.SIGN_BINARY", "tests/fake-bin-sign")

+ def test_cancel_script_failure(f_build_rpm_sign_on, caplog):

+     config = f_build_rpm_sign_on

+     worker = config.bw

+     config.ssh.set_command(

+         "copr-rpmbuild-log",

+         0, "canceled stdout\n", "canceled stderr\n",

+         _CancelFunction(worker),

+     )

+     config.ssh.set_command(

+         "copr-rpmbuild-cancel",

+         1, "output", "err output",

+     )

+     worker.process()

+     assert_logs_exist([

+         "Can't cancel build\nout:\noutput\nerr:\nerr output",

+         "Build was canceled",

+         COMMON_MSGS["not finished"],

+         "Worker failed build, took",

+     ], caplog)

+ 

+ @_patch_bwbuild_object("CANCEL_CHECK_PERIOD", 0.5)

+ @mock.patch("copr_backend.sign.SIGN_BINARY", "tests/fake-bin-sign")

+ def test_cancel_build_during_log_download(f_build_rpm_sign_on, caplog):

+     config = f_build_rpm_sign_on

+     worker = config.bw

+     config.ssh.set_command(

+         "copr-rpmbuild-log",

+         0, "canceled stdout\n", "canceled stderr\n",

+         _CancelFunction(worker),

+     )

+     config.ssh.set_command("copr-rpmbuild-cancel", 0, "out", "err")

+     worker.process()

+     assert_logs_exist([

+         "Cancel request succeeded\nout:\nouterr:\nerr",

+         "Build was canceled",

+         COMMON_MSGS["not finished"],

+     ], caplog)

+ 

+ def test_ssh_connection_error(f_build_rpm_case, caplog):

+     class _SideEffect:

+         counter = 0

+         def __call__(self):

+             self.counter += 1

+             if self.counter == 1:

+                 return (1, "err stdout", "err stderr")

+             return (0, "", "")

+ 

+     config = f_build_rpm_case

+     ssh = config.ssh

+     ssh.set_command("/usr/bin/test -f /etc/mock/fedora-30-x86_64.cfg",

+                     0, "", "", return_action=_SideEffect())

+     worker = config.bw

+     worker.process()

+     assert_logs_exist([

+         "Retry #1 (on other host)",

+         "Worker succeeded build",

+     ], caplog)

+ 

+ def test_average_step():

+     assert _average_step([]) == float("inf")

+     assert _average_step([1, 2, 3]) == float("inf")

+     assert _average_step([1, 2, 3, 4, 6]) == 1.25

+ 

+ @_patch_bwbuild_object("time.sleep", mock.MagicMock())

+ @_patch_bwbuild_object("time.time")

+ def test_retry_for_ssh_tail_failure(mc_time, f_build_rpm_case, caplog):

+     mc_time.side_effect = list(range(500))

+     class _SideEffect:

+         counter = 0

+         def __call__(self):

+             self.counter += 1

+             if self.counter > 5:

+                 return (0, "", "")

+             raise SSHConnectionError("test failure")

+     config = f_build_rpm_case

+     ssh = config.ssh

+     ssh.set_command("copr-rpmbuild-log",

+                     0, "", "", return_action=_SideEffect())

+     worker = config.bw

+     worker.process()

+     assert_logs_exist([

+         "Retry #1 (on other host)",

+         "Worker succeeded build",

+         "Giving up for unstable SSH",

+     ], caplog)

+     assert_messages_sent(["build.start", "chroot.start", "build.end"], worker.sender)

+ 

+ def test_build_failure(f_build_rpm_case, caplog):

+     config = f_build_rpm_case

+     config.ssh.unlink_success = True

+     worker = config.bw

+     worker.process()

+     assert_logs_exist([

+         "Backend process error: No success file => build failure",

+         "Worker failed build, took ",

+         "Finished build: id=848963 failed=True ",

+     ], caplog)

+     assert_messages_sent(["build.start", "chroot.start", "build.end"], worker.sender)

+ 

+ @_patch_bwbuild_object("call_copr_repo")

+ def test_createrepo_failure(mc_call_copr_repo, f_build_rpm_case, caplog):

+     mc_call_copr_repo.return_value = False

+     config = f_build_rpm_case

+     worker = config.bw

+     worker.process()

+     assert_logs_exist([

+         "Backend process error: createrepo failed",

+         "Worker failed build, took ",

+         "Finished build: id=848963 failed=True ",

+     ], caplog)

+ 

+ @_patch_bwbuild_object("pkg_name_evr")

+ def test_pkg_collect_failure(mc_pkg_evr, f_build_srpm, caplog):

+     mc_pkg_evr.side_effect = CoprBackendSrpmError("srpm error")

+     config = f_build_srpm

+     worker = config.bw

+     worker.process()

+     assert_logs_exist([

+         "Error while collecting built packages",

+         "Worker failed build, took ",

+         "Finished build: id=855954 failed=True ",

+     ], caplog)

+     assert worker.job.status == 0  # fail

+ 

+ def test_existing_compressed_file(f_build_rpm_case, caplog):

+     config = f_build_rpm_case

+     config.ssh.precreate_compressed_log_file = True

+     worker = config.bw

+     worker.process()

+     assert_logs_exist([

+         "Worker succeeded build, took ",

+         "builder-live.log.gz exists",

+         "Finished build: id=848963 failed=False ",  # still success!

+     ], caplog)

+ 

+ def test_tail_f_nonzero_exit(f_build_rpm_case, caplog):

+     config = f_build_rpm_case

+     worker = config.bw

+     class _SideEffect:

+         counter = 0

+         def __call__(self):

+             self.counter += 1

+             if self.counter > 3:

+                 return (0, "ok\n", "ok\n")

+             return (1, "fail out\n", "fail err\n")

+     config.ssh.set_command(

+         "copr-rpmbuild-log",

+         0, "failed stdout\n", "failed stderr\n",

+         return_action=_SideEffect(),

+     )

+     worker.process()

+     assert_logs_exist([

+         "Retry #3 (on other host)",

+         "Worker succeeded build, took ",

+         "Finished build: id=848963 failed=False ",  # still success!

+     ], caplog)

+ 

+ def test_wrong_copr_rpmbuild_daemon_output(f_build_srpm, caplog):

+     config = f_build_srpm

+     config.ssh.set_command(

+         "copr-rpmbuild --verbose --drop-resultdir --srpm --build-id 855954 "

+         "--detached",

+         0, "6a66", "",

+     )

+     config.bw.process()

+     assert_logs_exist([

+         "Backend process error: copr-rpmbuild returned invalid"

+         " PID on stdout: 6a66",

+         "Worker failed build, took ",

+         "builder-live.log: No such file or directory",

+     ], caplog)

+     assert_logs_dont_exist([

+         "Retry",

+         "Finished build",

+     ], caplog)

+ 

+ def test_unable_to_start_builder(f_build_srpm, caplog):

+     config = f_build_srpm

+     config.ssh.set_command(

+         "copr-rpmbuild --verbose --drop-resultdir --srpm --build-id 855954 "

+         "--detached",

+         10, "stdout\n", "stderr\n",

+     )

+     config.bw.process()

+     assert_logs_exist([

+         "Can't start copr-rpmbuild",

+         "out:\nstdout\nerr:\nstderr\n",

+         "builder-live.log: No such file or directory",

+     ], caplog)

+     assert_logs_dont_exist(["Retry"], caplog)

+ 

+ @_patch_bwbuild_object("time.sleep", mock.MagicMock())

+ def test_retry_vm_factory_take(f_build_srpm, caplog):

+     config = f_build_srpm

+     rhf = config.resalloc_host_factory

+     host = config.host

+     fake_host = Munch()

+     fake_host.wait_ready = lambda: False

+     fake_host.release = lambda: None

+     fake_host.info = "fake host"

+     rhf.return_value.get_host.side_effect = [fake_host, host]

+     config.bw.process()

+     assert_logs_exist([

+         "VM allocation failed, trying to allocate new VM",

+         "Finished build: id=855954 failed=False",

+     ], caplog)

+     assert config.bw.job.status == 1  # success

+ 

+ def test_failed_build_retry(f_build_rpm_case, caplog):

+     config = f_build_rpm_case

+     rhf = config.resalloc_host_factory

+     hosts = [_fake_host() for _ in range(4)]

+     for index in range(4):

+         hosts[index].hostname = "1.2.3." + str(index)

+     rhf.return_value.get_host.side_effect = hosts

+     ssh = config.ssh

+     ssh.set_command("/usr/bin/test -f /etc/mock/fedora-30-x86_64.cfg",

+                     1, "", "not found")

+ 

+     config.bw.process()

+     assert_logs_exist([

+         "Three host tried without success: {'1.2.3.",

+         COMMON_MSGS["not finished"],

+         "Worker failed build, took ",

+     ], caplog)

+     assert config.bw.job.status == 0

+     # Only build.end sent by this worker (after reset)

+     assert_messages_sent(["build.end"], config.bw.sender)

@@ -4,7 +4,7 @@

  import json

  import logging

  

- from copr_backend.exceptions import BuilderError

+ from copr_backend.background_worker_build import BackendError

  from copr_backend.helpers import get_redis_logger, get_chroot_arch, \

          format_filename, get_redis_connection

  from copr_backend.constants import LOG_REDIS_FIFO
@@ -34,7 +34,7 @@

      def test_redis_logger_exception(self):

          log = get_redis_logger(self.opts, "copr_backend.test", "test")

          try:

-             raise BuilderError("foobar")

+             raise BackendError("foobar")

          except Exception as err:

              log.exception("error occurred: {}".format(err))

  
@@ -42,7 +42,8 @@

          data = json.loads(raw_message)

          assert data.get("who") == "test"

          assert data.get("levelno") == logging.ERROR

-         assert 'copr_backend.exceptions.BuilderError: foobar\n' in data['msg']

+         assert "error occurred: Backend process error: foobar\n" in data["msg"]

+         assert 'raise BackendError("foobar")' in data["msg"]

  

      def test_get_chroot_arch(self):

          assert get_chroot_arch("fedora-26-x86_64") == "x86_64"

@@ -0,0 +1,188 @@

+ """

+ Library for testing copr-backend

+ """

+ 

+ import json

+ import os

+ import shutil

+ from unittest.mock import MagicMock

+ 

+ from copr_backend.background_worker_build import COMMANDS

+ from copr_backend.sshcmd import SSHConnection, SSHConnectionError

+ 

+ 

+ def minimal_be_config(where):

+     """

+     Create minimal be config which is parseable by BackendConfigReader.

+     """

+ 

+     destdir = os.path.join(where, "results")

+     try:

+         os.mkdir(destdir)

+     except FileExistsError:

+         pass

+ 

+     minimal_config_snippet = (

+         "[backend]\n"

+         "destdir={}\n"

+         "redis_port=7777\n"

+         "results_baseurl=https://example.com/results\n"

+     ).format(destdir)

+ 

+     be_config_file = os.path.join(where, "copr-be.conf")

+     with open(be_config_file, "w") as cfg_fd:

+         cfg_fd.write(minimal_config_snippet)

+     return be_config_file

+ 

+ 

+ VALID_RPM_JOB = {

+     "build_id": 848963,

+     "buildroot_pkgs": [],

+     "chroot": "fedora-30-x86_64",

+     "enable_net": False,

+     "fetch_sources_only": True,

+     "git_hash": "f9189466300e97944eaa9e581aec7a2c3453823d",

+     "git_repo": "@copr/TEST1575431880356948981Project10/example",

+     "memory_reqs": 2048,

+     "package_name": "example",

+     "package_version": "1.0.14-1.fc31",

+     "project_dirname": "TEST1575431880356948981Project10",

+     "project_name": "TEST1575431880356948981Project10",

+     "project_owner": "@copr",

+     "repos": [{

+         "baseurl": "https://download.copr-dev.fedorainfracloud.org/"

+                    "results/@copr/TEST1575431880356948981Project10/fedora-30-x86_64/",

+         "id": "copr_base",

+         "name": "Copr repository"

+     }],

+     "sandbox": "@copr/TEST1575431880356948981Project10--praiskup",

+     "source_json": json.dumps({

+         "clone_url": "https://copr-dist-git-dev.fedorainfracloud.org/"

+                      "git/@copr/TEST1575431880356948981Project10/"

+                      "example.git",

+         "committish": "f9189466300e97944eaa9e581aec7a2c3453823d",

+     }),

+     "source_type": 8,

+     "submitter": "praiskup",

+     "task_id": "848963-fedora-30-x86_64",

+     "timeout": 75600,

+     "use_bootstrap_container": False,

+     "uses_devel_repo": False,

+     "with_opts": [],

+     "without_opts": []

+ }

+ 

+ 

+ VALID_SRPM_JOB = {

+     "build_id": 855954,

+     "chroot": None,

+     "project_dirname": "PROJECT_2",

+     "project_name": "PROJECT_2",

+     "project_owner": "@copr",

+     "sandbox": "@copr/PROJECT_2--praiskup",

+     "source_json": json.dumps({

+         "type": "git",

+         "clone_url": "https://pagure.io/copr/copr-hello.git",

+         "committish": "",

+         "subdirectory": "",

+         "spec": "",

+         "srpm_build_method": "rpkg",

+     }),

+     "source_type": 8,

+     "submitter": "praiskup",

+     "task_id": "855954"

+ }

+ 

+ 

+ class TimeSequenceSideEffect:

+     """

+     Mimic time.time(), and at special time call special function

+     """

+     def __init__(self, sequence, special_cases):

+         self.sequence = sequence

+         self.special_cases = special_cases

+         self.counter = 0

+ 

+     def __call__(self):

+         retval = self.sequence[self.counter]

+         self.counter += 1

+         if retval in self.special_cases:

+             self.special_cases[retval]()

+         return retval

+ 

+ 

+ class FakeSSHConnection(SSHConnection):

+     """ replacement for SSHConnection """

+     unlink_success = False

+     precreate_compressed_log_file = False

+ 

+     def __init__(self, user=None, host=None, config_file=None, log=None):

+         _unused = user, host, config_file

+         super().__init__(log=log)

+         self.commands = {}

+         self.set_command(COMMANDS["rpm_q_builder"],

+                          0, "666\n", "")

+         self.set_command("/usr/bin/test -f /etc/mock/fedora-30-x86_64.cfg",

+                          0, "", "")

+         self.set_command("copr-rpmbuild-log",

+                          0, "build log stdout\n", "build log stderr\n")

+ 

+     def set_command(self, cmd, exit_code, stdout, stderr, action=None,

+                     return_action=None):

+         """ setup expected output """

+         self.commands[cmd] = (exit_code, stdout, stderr, action, return_action)

+ 

+     def get_command(self, cmd):

+         """ get predefined command output, and call the action """

+         try:

+             res = self.commands[cmd]

+             if res[3]:

+                 res[3]()

+             if res[4]:

+                 return res[4]()

+             return res

+         except KeyError:

+             raise SSHConnectionError("undefined cmd '{}' in FakeSSHConnection"

+                                      .format(cmd))

+ 

+     def run(self, user_command, stdout=None, stderr=None, max_retries=0):

+         """ fake SSHConnection.run() """

+         with open(os.devnull, "w") as devnull:

+             out = stdout or devnull

+             err = stderr or devnull

+             res = self.get_command(user_command)

+             out.write(res[1])

+             err.write(res[2])

+             return res[0]

+ 

+     def run_expensive(self, user_command, max_retries=0):

+         """ fake SSHConnection.run_expensive() """

+         res = self.get_command(user_command)

+         return (res[0], res[1], res[2])

+ 

+     def _ssh_base(self):

+         return ["ssh"]

+ 

+     def _full_source_path(self, src):

+         return src

+ 

+     def rsync_download(self, src, dest, logfile=None, max_retries=0):

+         data = os.environ["TEST_DATA_DIRECTORY"]

+         trail_slash = src.endswith("/")

+         src = os.path.join(data, "build_results", "00848963-example")

+         if trail_slash:

+             src = src + "/"

+ 

+         self.log.info("rsync from src=%s to dest=%s", src, dest)

+ 

+         super().rsync_download(src, dest, logfile)

+         os.unlink(os.path.join(dest, "backend.log.gz"))

+ 

+         if not self.precreate_compressed_log_file:

+             os.unlink(os.path.join(dest, "builder-live.log.gz"))

+ 

+         if self.unlink_success:

+             os.unlink(os.path.join(dest, "success"))

+ 

+         if "PROJECT_2" in dest:

+             os.unlink(os.path.join(dest, "example-1.0.14-1.fc30.x86_64.rpm"))

@@ -1,176 +0,0 @@

- # coding: utf-8

- import json

- import shutil

- from subprocess import CalledProcessError

- import tempfile

- import time

- from multiprocessing import Queue

- import types

- 

- from munch import Munch

- from redis import ConnectionError

- from copr_backend.exceptions import CoprSpawnFailError

- 

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage import EventTopics, PUBSUB_MB

- from copr_backend.vm_manage.check import HealthChecker, check_health

- 

- from unittest import mock, skip

- from unittest.mock import MagicMock

- import pytest

- 

- 

- """

- REQUIRES RUNNING REDIS

- TODO: look if https://github.com/locationlabs/mockredis can be used

- """

- 

- MODULE_REF = "copr_backend.vm_manage.check"

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_terminate_vm():

-     with mock.patch("{}.terminate_vm".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_process():

-     with mock.patch("{}.Process".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_run_ans():

-     with mock.patch("{}.run_ansible_playbook_once".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_ans_runner():

-     yield object()

- 

- 

- @pytest.yield_fixture

- def mc_grc():

-     with mock.patch("{}.get_redis_connection".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- class TestChecker(object):

- 

-     def setup_method(self, method):

-         self.test_root_path = tempfile.mkdtemp()

-         self.terminate_pb_path = "{}/terminate.yml".format(self.test_root_path)

-         self.opts = Munch(

-             redis_db=9,

-             redis_port=7777,

-             ssh=Munch(

-                 transport="ssh"

-             ),

-             build_groups={

-                 0: {

-                     "terminate_playbook": self.terminate_pb_path,

-                     "name": "base",

-                     "archs": ["i386", "x86_64"],

-                 }

-             },

- 

-             build_user="mockbuilder",

-             fedmsg_enabled=False,

-             sleeptime=0.1,

-             do_sign=True,

-             timeout=1800,

-             results_baseurl="/tmp",

-             vm_ssh_check_timeout=2,

-         )

-         # self.try_spawn_args = '-c ssh {}'.format(self.spawn_pb_path)

- 

-         # self.callback = TestCallback()

-         self.grl_patcher = mock.patch("{}.get_redis_logger".format(MODULE_REF))

-         self.grl_patcher.start()

- 

-         self.checker = MagicMock()

-         self.terminator = MagicMock()

- 

-         self.checker = HealthChecker(self.opts)

-         self.checker.recycle = types.MethodType(mock.MagicMock, self.terminator)

-         self.vm_ip = "127.0.0.1"

-         self.vm_name = "localhost"

-         self.group = 0

-         self.username = "bob"

- 

-         self.rc = get_redis_connection(self.opts)

- 

-     def teardown_method(self, method):

-         self.grl_patcher.stop()

-         shutil.rmtree(self.test_root_path)

-         keys = self.rc.keys("*")

-         if keys:

-             self.rc.delete(*keys)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_health_runner_no_response(self, mc_ans_runner, mc_grc):

-         mc_runner = MagicMock()

-         mc_ans_runner.return_value = mc_runner

-         # mc_runner.connection.side_effect = IOError()

- 

-         mc_rc = MagicMock()

-         mc_grc.return_value = mc_rc

- 

-         # didn't raise exception

-         check_health(self.opts, self.vm_name, self.vm_ip)

-         assert mc_rc.publish.call_args[0][0] == PUBSUB_MB

-         dict_result = json.loads(mc_rc.publish.call_args[0][1])

-         assert dict_result["result"] == "failed"

-         assert "VM is not responding to the testing playbook." in dict_result["msg"]

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_health_runner_exception(self, mc_ans_runner, mc_grc):

-         mc_conn = MagicMock()

-         mc_ans_runner.return_value = mc_conn

-         mc_conn.run.side_effect = IOError()

- 

-         mc_rc = MagicMock()

-         mc_grc.return_value = mc_rc

- 

-         # didn't raise exception

-         check_health(self.opts, self.vm_name, self.vm_ip)

-         assert mc_rc.publish.call_args[0][0] == PUBSUB_MB

-         dict_result = json.loads(mc_rc.publish.call_args[0][1])

-         assert dict_result["result"] == "failed"

-         assert "Failed to check  VM" in dict_result["msg"]

-         assert "due to ansible error:" in dict_result["msg"]

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_health_runner_ok(self, mc_ans_runner, mc_grc):

-         mc_conn = MagicMock()

-         mc_ans_runner.return_value = mc_conn

-         mc_conn.run.return_value = {"contacted": [self.vm_ip]}

- 

-         mc_rc = MagicMock()

-         mc_grc.return_value = mc_rc

- 

-         # didn't raise exception

-         check_health(self.opts, self.vm_name, self.vm_ip)

-         assert mc_rc.publish.call_args[0][0] == PUBSUB_MB

-         dict_result = json.loads(mc_rc.publish.call_args[0][1])

-         assert dict_result["result"] == "OK"

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_check_health_pubsub_publish_error(self, mc_ans_runner, mc_grc):

-         mc_conn = MagicMock()

-         mc_ans_runner.return_value = mc_conn

-         mc_conn.run.return_value = {"contacted": [self.vm_ip]}

- 

-         mc_grc.side_effect = ConnectionError()

- 

-         # didn't raise exception

-         check_health(self.opts, self.vm_name, self.vm_ip)

- 

-         assert mc_conn.run.called

-         assert mc_grc.called

@@ -1,361 +0,0 @@

- # coding: utf-8

- import json

- import shutil

- import tempfile

- import time

- from multiprocessing import Queue

- import types

- 

- from munch import Munch

- from redis.client import Script

- 

- from copr_backend.exceptions import VmDescriptorNotFound

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage import VmStates

- from copr_backend.vm_manage.event_handle import EventHandler, Recycle

- from copr_backend.vm_manage.models import VmDescriptor

- 

- from unittest import mock, skip

- from unittest.mock import MagicMock

- import pytest

- 

- 

- """

- REQUIRES RUNNING REDIS

- TODO: look if https://github.com/locationlabs/mockredis can be used

- """

- 

- MODULE_REF = "copr_backend.vm_manage.event_handle"

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_setproctitle():

-     with mock.patch("{}.setproctitle".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_process():

-     with mock.patch("{}.Process".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_recycle():

-     with mock.patch("{}.Recycle".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_run_ans():

-     with mock.patch("{}.run_ansible_playbook_once".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_grc():

-     with mock.patch("{}.get_redis_connection".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_grl():

-     with mock.patch("{}.get_redis_logger".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- class TestEventHandle(object):

- 

-     def setup_method(self, method):

-         self.test_root_path = tempfile.mkdtemp()

-         self.terminate_pb_path = "{}/terminate.yml".format(self.test_root_path)

-         self.opts = Munch(

-             redis_db=9,

-             redis_port=7777,

-             ssh=Munch(

-                 transport="ssh"

-             ),

-             build_groups={

-                 0: {

-                     "terminate_playbook": self.terminate_pb_path,

-                     "name": "base",

-                     "archs": ["i386", "x86_64"],

-                     "vm_max_check_fails": 2,

-                 }

-             },

- 

-             fedmsg_enabled=False,

-             sleeptime=0.1,

-             do_sign=True,

-             timeout=1800,

-             # destdir=self.tmp_dir_path,

-             results_baseurl="/tmp",

-         )

-         self.rc = get_redis_connection(self.opts)

- 

-         self.checker = MagicMock()

-         self.spawner = MagicMock()

-         self.terminator = MagicMock()

- 

-         self.queue = Queue()

-         self.vmm = MagicMock()

-         self.vmm.rc = self.rc

- 

-         self.grl_patcher = mock.patch("{}.get_redis_logger".format(MODULE_REF))

-         self.grl_patcher.start()

- 

-         self.eh = EventHandler(self.opts,

-                                self.vmm,

-                                self.terminator)

-         self.eh.post_init()

- 

-         self.vm_ip = "127.0.0.1"

-         self.vm_name = "localhost"

-         self.group = 0

-         self.username = "bob"

- 

-         self.msg = {"vm_ip": self.vm_ip, "vm_name": self.vm_name, "group": self.group}

-         self.stage = 0

- 

-     def erase_redis(self):

-         keys = self.rc.keys("*")

-         if keys:

-             self.rc.delete(*keys)

- 

-     def teardown_method(self, method):

-         self.grl_patcher.stop()

-         shutil.rmtree(self.test_root_path)

-         self.erase_redis()

- 

-     def test_post_init(self):

-         test_eh = EventHandler(self.opts, self.vmm, self.terminator)

-         assert "on_health_check_success" not in test_eh.lua_scripts

-         test_eh.post_init()

-         assert test_eh.lua_scripts["on_health_check_success"]

-         assert isinstance(test_eh.lua_scripts["on_health_check_success"], Script)

- 

-     def test_recycle(self, mc_time):

-         self.recycle = Recycle(terminator=self.terminator, recycle_period=60)

-         self.stage = 0

- 

-         def incr(*args, **kwargs):

-             self.stage += 1

-             if self.stage > 2:

-                 self.recycle.terminate()

-         mc_time.sleep.side_effect = incr

- 

-         assert not self.terminator.recycle.called

-         self.recycle.run()

-         assert self.terminator.recycle.called

-         assert len(self.terminator.recycle.call_args_list) == 3

- 

-     def test_on_vm_spawned(self):

-         expected_call = mock.call(**self.msg)

-         self.eh.on_vm_spawned(self.msg)

-         assert self.vmm.add_vm_to_pool.call_args == expected_call

- 

-     def test_on_vm_termination_request(self):

-         expected_call = mock.call(**self.msg)

-         self.eh.on_vm_termination_request(self.msg)

-         assert self.terminator.terminate_vm.call_args == expected_call

- 

-     def test_health_check_result_no_vmd(self):

-         self.vmm.get_vm_by_name.side_effect = VmDescriptorNotFound("foobar")

-         self.eh.lua_scripts = MagicMock()

- 

-         self.eh.on_health_check_result(self.msg)

-         assert not self.eh.lua_scripts["on_health_check_success"].called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_health_check_result_on_ok(self):

-         # on success should change state from "check_health" to "ready"

-         # and reset check fails to zero

-         self.vmd = VmDescriptor(self.vm_ip, self.vm_name, self.group, VmStates.CHECK_HEALTH)

-         self.vmd.store(self.rc)

-         self.vmd.store_field(self.rc, "check_fails", 1)

- 

-         self.vmm.get_vm_by_name.return_value = self.vmd

-         msg = self.msg

-         msg["result"] = "OK"

- 

-         self.eh.on_health_check_result(msg)

-         assert self.vmd.get_field(self.rc, "state") == VmStates.READY

-         assert int(self.vmd.get_field(self.rc, "check_fails")) == 0

- 

-         # if old state in "in_use" don't change it

-         self.vmd.store_field(self.rc, "state", VmStates.IN_USE)

-         self.vmd.store_field(self.rc, "check_fails", 1)

-         self.eh.on_health_check_result(msg)

- 

-         assert self.vmd.get_field(self.rc, "state") == VmStates.IN_USE

-         assert int(self.vmd.get_field(self.rc, "check_fails")) == 0

- 

-         # if old state not in ["in_use", "check_health"] don't touch it

-         # and also don't reset check_fails

-         self.vmd.store_field(self.rc, "check_fails", 1)

-         for state in [VmStates.TERMINATING, VmStates.GOT_IP, VmStates.READY]:

-             self.vmd.store_field(self.rc, "state", state)

-             self.eh.on_health_check_result(msg)

- 

-             assert int(self.vmd.get_field(self.rc, "check_fails")) == 1

-             assert self.vmd.get_field(self.rc, "state") == state

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_health_check_result_on_fail_from_check_health(self):

-         # on fail set state to check failed state and increment fails counter

-         self.vmd = VmDescriptor(self.vm_ip, self.vm_name, self.group, VmStates.CHECK_HEALTH)

-         self.vmd.store(self.rc)

- 

-         self.vmm.get_vm_by_name.return_value = self.vmd

-         msg = self.msg

-         msg["result"] = "failed"

- 

-         assert self.vmd.get_field(self.rc, "state") == VmStates.CHECK_HEALTH

-         self.eh.on_health_check_result(msg)

-         assert self.vmd.get_field(self.rc, "state") == VmStates.CHECK_HEALTH_FAILED

-         assert int(self.vmd.get_field(self.rc, "check_fails")) == 1

-         self.eh.on_health_check_result(msg)

-         assert int(self.vmd.get_field(self.rc, "check_fails")) == 2

- 

-         # when threshold exceeded request termination

-         self.eh.on_health_check_result(msg)

-         assert self.vmm.start_vm_termination.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_health_check_result_on_fail_from_in_use(self):

-         # on fail set state to check failed state and increment fails counter

-         self.vmd = VmDescriptor(self.vm_ip, self.vm_name, self.group, VmStates.IN_USE)

-         self.vmd.store(self.rc)

- 

-         self.vmm.get_vm_by_name.return_value = self.vmd

-         msg = self.msg

-         msg["result"] = "failed"

- 

-         assert self.vmd.get_field(self.rc, "state") == VmStates.IN_USE

-         self.eh.on_health_check_result(msg)

-         assert self.vmd.get_field(self.rc, "state") == VmStates.IN_USE

-         assert int(self.vmd.get_field(self.rc, "check_fails")) == 1

-         self.eh.on_health_check_result(msg)

-         assert self.vmd.get_field(self.rc, "state") == VmStates.IN_USE

-         assert int(self.vmd.get_field(self.rc, "check_fails")) == 2

- 

-         # when threshold exceeded request termination do NOT terminate it

-         self.eh.on_health_check_result(msg)

-         assert self.vmd.get_field(self.rc, "state") == VmStates.IN_USE

-         assert not self.vmm.start_vm_termination.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_health_check_result_on_wrong_states(self):

-         self.vmd = VmDescriptor(self.vm_ip, self.vm_name, self.group, VmStates.GOT_IP)

-         self.vmd.store(self.rc)

-         self.vmm.get_vm_by_name.return_value = self.vmd

- 

-         self.vmd.store_field(self.rc, "check_fails", 100)

-         msg = self.msg

-         msg["result"] = "failed"

-         for state in [VmStates.TERMINATING, VmStates.GOT_IP, VmStates.READY]:

-             self.vmd.store_field(self.rc, "state", state)

-             self.eh.on_health_check_result(msg)

- 

-             assert int(self.vmd.get_field(self.rc, "check_fails")) == 100

-             assert self.vmd.get_field(self.rc, "state") == state

-             assert not self.vmm.terminate_vm.called

- 

-     def test_on_vm_termination_result_ok(self):

-         msg = self.msg

-         msg["result"] = "OK"

-         self.eh.on_vm_termination_result(msg)

-         assert self.vmm.remove_vm_from_pool.called

-         msg.pop("vm_name")

-         self.vmm.remove_vm_from_pool.reset_mock()

-         self.eh.on_vm_termination_result(msg)

-         assert not self.vmm.remove_vm_from_pool.called

- 

-     def test_on_vm_termination_result_fail(self):

-         msg = self.msg

-         msg["result"] = "failed"

-         self.eh.on_vm_termination_result(msg)

-         assert not self.vmm.remove_vm_from_pool.called

- 

-     def test_dummy_run(self, mc_setproctitle, mc_recycle):

-         #  dummy test, mainly for perfect coverage

-         self.eh.start_listen = types.MethodType(MagicMock(), self.eh)

-         self.eh.run()

- 

-         assert mc_recycle.called

-         assert self.eh.start_listen.called

- 

-     def test_dummy_terminate(self, mc_setproctitle, mc_recycle):

-         #  dummy test, mainly for perfect coverage

-         assert not self.eh.kill_received

-         self.eh.do_recycle_proc = MagicMock()

- 

-         self.eh.terminate()

- 

-         assert self.eh.kill_received

-         assert self.eh.do_recycle_proc.terminate.called

-         assert self.eh.do_recycle_proc.join.called

- 

-     def test_start_test_listen(self):

-         self.vmm.rc = MagicMock()

-         mc_channel = MagicMock()

-         self.vmm.rc.pubsub.return_value = mc_channel

- 

-         self.eh.handlers_map = MagicMock()

- 

-         def on_listen():

-             if self.stage == 1:

-                 return None

-             elif self.stage == 2:

-                 assert not self.eh.handlers_map.__getitem__.called

-                 return {}

-             elif self.stage == 3:

-                 assert not self.eh.handlers_map.__getitem__.called

-                 return {"type": "subscribe"}

-             elif self.stage == 4:

-                 assert not self.eh.handlers_map.__getitem__.called

-                 return {"type": "message"}

-             elif self.stage == 5:

-                 assert not self.eh.handlers_map.__getitem__.called

-                 return {"type": "message", "data": "{{"}

-             elif self.stage == 6:

-                 assert not self.eh.handlers_map.__getitem__.called

-                 return {"type": "message", "data": json.dumps({})}  # no topic

-             elif self.stage == 7:

-                 assert not self.eh.handlers_map.__getitem__.called

-                 self.eh.handlers_map.__contains__.return_value = False               #

-                 self.eh.handlers_map.__getitem__.side_effect = KeyError()            #

-                 return {"type": "message", "data": json.dumps({"topic": "foobar"})}  # no handler for topic

-             elif self.stage == 8:

-                 # import ipdb; ipdb.set_trace()

-                 assert not self.eh.handlers_map.__getitem__.called

-                 assert self.eh.handlers_map.__contains__.called

- 

-                 self.eh.handlers_map.__contains__.return_value = True

-                 self.eh.handlers_map.__contains__.reset_mock()

-                 self.eh.handlers_map.__getitem__.reset_mock()

-                 self.eh.handlers_map.__getitem__.return_value.side_effect = IOError()  #

-                 return {"type": "message", "data": json.dumps({"topic": "foobar"})}    # handler produces exception

-             elif self.stage == 9:

-                 assert self.eh.handlers_map.__getitem__.called

-                 assert self.eh.handlers_map.__contains__.called

- 

-                 self.eh.handlers_map.__contains__.return_value = True

-                 self.eh.handlers_map.__getitem__.reset_mock()

-                 self.eh.handlers_map.__getitem__.return_value = None

-                 return {"type": "message", "data": json.dumps({"topic": "foobar"})}    # handler invoked ok

- 

-             else:

-                 self.eh.kill_received = True

- 

-         def my_gen():

-             self.stage += 1

-             while True:

-                 yield on_listen()

-                 self.stage += 1

- 

-         mc_channel.listen.return_value = my_gen()

- 

-         self.eh.start_listen()

-         assert mc_channel.listen.called

@@ -1,99 +0,0 @@

- # coding: utf-8

- 

- from multiprocessing import Queue

- import types

- 

- from munch import Munch

- import time

- 

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage.executor import Executor

- 

- from unittest import mock

- import pytest

- 

- 

- """

- REQUIRES RUNNING REDIS

- TODO: look if https://github.com/locationlabs/mockredis can be used

- """

- 

- MODULE_REF = "copr_backend.vm_manage.executor"

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- class TestExecutor(object):

- 

-     def setup_method(self, method):

-         self.opts = Munch(

-             redis_db=9,

-             redis_port=7777,

-             ssh=Munch(

-                 transport="ssh"

-             ),

-             build_groups={

-                 0: {

-                     "spawn_playbook": "/spawn.yml",

-                     "name": "base",

-                     "archs": ["i386", "x86_64"]

-                 }

-             }

-         )

- 

-         self.executor = Executor(self.opts)

-         self.rc = get_redis_connection(self.opts)

- 

-     def teardown_method(self, method):

-         keys = self.rc.keys("*")

-         if keys:

-             self.rc.delete(*keys)

- 

-     def test_recycle(self, mc_time):

-         self.executor.last_recycle = 0

-         mc_time.time.return_value = int(1.1 * self.executor.recycle_period)

-         p1 = mock.MagicMock()

-         p2 = mock.MagicMock()

-         self.executor.child_processes.extend([p1, p2])

-         p1.is_alive.return_value = True

-         p2.is_alive.return_value = False

- 

-         self.executor.recycle()

- 

-         assert len(self.executor.child_processes) == 1

-         assert self.executor.child_processes[0] == p1

-         assert p2.join.called

- 

-         self.executor.last_recycle = self.executor.recycle_period

-         p1.reset_mock()

-         assert not p1.is_alive.called

-         self.executor.recycle()

-         assert not p1.is_alive.called

-         self.executor.recycle(force=True)

-         assert p1.is_alive.called

- 

-     def test_terminate(self):

-         p1 = mock.MagicMock()

-         p2 = mock.MagicMock()

-         self.executor.child_processes.extend([p1, p2])

- 

-         self.executor.terminate()

-         assert p1.terminate.called

-         assert p2.terminate.called

-         assert p1.join.called

-         assert p2.join.called

- 

-     def test_children_number(self):

-         mm = mock.MagicMock()

-         self.executor.recycle = types.MethodType(mm, self.executor)

-         assert self.executor.children_number == 0

-         assert self.executor.recycle.called

- 

-         p1 = mock.MagicMock()

-         p2 = mock.MagicMock()

- 

-         self.executor.child_processes.extend([p1, p2])

-         assert self.executor.children_number == 2

@@ -1,442 +0,0 @@

- # coding: utf-8

- import json

- import random

- 

- import types

- import time

- from multiprocessing import Queue

- 

- from munch import Munch

- 

- from copr_backend import exceptions

- from copr_backend.exceptions import VmError, NoVmAvailable

- from copr_backend.vm_manage import VmStates, KEY_VM_POOL, PUBSUB_MB, EventTopics, KEY_SERVER_INFO

- from copr_backend.vm_manage.manager import VmManager

- from copr_backend.daemons.vm_master import VmMaster

- from copr_backend.helpers import get_redis_connection

- 

- from  unittest import mock

- from unittest.mock import MagicMock

- import pytest

- 

- 

- """

- REQUIRES RUNNING REDIS

- TODO: look if https://github.com/locationlabs/mockredis can be used

- """

- 

- MODULE_REF = "copr_backend.vm_manage.manager"

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- GID1 = 0

- GID2 = 1

- 

- # some sandbox string, for tests where we don't care about its value

- SANDBOX = 'sandbox'

- 

- class TestManager(object):

- 

-     def setup_method(self, method):

-         self.opts = Munch(

-             redis_db=9,

-             redis_port=7777,

-             ssh=Munch(

-                 transport="ssh"

-             ),

-             build_groups_count=2,

-             build_groups={

-                 GID1: {

-                     "name": "base",

-                     "archs": ["i386", "x86_64"],

-                     "max_vm_per_user": 3,

-                 },

-                 GID2: {

-                     "name": "arm",

-                     "archs": ["armV7",]

-                 }

-             },

- 

-             fedmsg_enabled=False,

-             sleeptime=0.1,

-             do_sign=True,

-             timeout=1800,

-             # destdir=self.tmp_dir_path,

-             results_baseurl="/tmp",

-         )

- 

-         self.vm_ip = "127.0.0.1"

-         self.vm_name = "localhost"

- 

-         self.vm2_ip = "127.0.0.2"

-         self.vm2_name = "localhost2"

- 

-         self.ownername = "bob"

- 

-         self.rc = get_redis_connection(self.opts)

-         self.ps = None

-         self.log_msg_list = []

- 

-         self.vmm = VmManager(self.opts)

-         self.vmm.log = MagicMock()

-         self.pid = 12345

- 

-     def teardown_method(self, method):

-         keys = self.vmm.rc.keys("*")

-         if keys:

-             self.vmm.rc.delete(*keys)

- 

-     def test_manager_setup(self):

-         vmm = VmManager(self.opts)

-         assert GID1 in vmm.vm_groups

-         assert GID2 in vmm.vm_groups

-         assert len(vmm.vm_groups) == 2

- 

-     def test_add_vm_to_pool(self):

-         self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

- 

-         with pytest.raises(VmError):

-             self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

- 

-         vm_list = self.vmm.get_all_vm_in_group(GID1)

-         vm = self.vmm.get_vm_by_name(self.vm_name)

- 

-         assert len(vm_list) == 1

-         assert vm_list[0].__dict__ == vm.__dict__

-         assert vm.vm_ip == self.vm_ip

-         assert vm.vm_name == self.vm_name

-         assert vm.group == GID1

- 

-     def test_mark_vm_check_failed(self, mc_time):

-         self.vmm.start_vm_termination = types.MethodType(MagicMock(), self.vmm)

-         self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd = self.vmm.get_vm_by_name(self.vm_name)

-         vmd.store_field(self.rc, "state", VmStates.CHECK_HEALTH)

-         vmd.store_field(self.rc, "last_health_check", 12345)

- 

-         self.vmm.mark_vm_check_failed(self.vm_name)

- 

-         assert vmd.get_field(self.rc, "state") == VmStates.CHECK_HEALTH_FAILED

-         states = [VmStates.GOT_IP, VmStates.IN_USE, VmStates.READY, VmStates.TERMINATING]

-         for state in states:

-             vmd.store_field(self.rc, "state", state)

-             self.vmm.mark_vm_check_failed(self.vm_name)

-             assert vmd.get_field(self.rc, "state") == state

- 

-     def test_acquire_vm_no_vm_after_server_restart(self, mc_time):

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd.store_field(self.rc, "state", VmStates.READY)

- 

-         # undefined both last_health_check and server_start_timestamp

-         mc_time.time.return_value = 0.1

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm([GID1], self.ownername, 42, SANDBOX)

- 

-         # only server start timestamp is defined

-         mc_time.time.return_value = 1

-         self.vmm.mark_server_start()

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm([GID1], self.ownername, 42, SANDBOX)

- 

-         # only last_health_check defined

-         self.rc.delete(KEY_SERVER_INFO)

-         vmd.store_field(self.rc, "last_health_check", 0)

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm([GID1], self.ownername, 42, SANDBOX)

- 

-         # both defined but last_health_check < server_start_time

-         self.vmm.mark_server_start()

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm([GID1], self.ownername, 42, SANDBOX)

- 

-         # and finally last_health_check > server_start_time

-         vmd.store_field(self.rc, "last_health_check", 2)

-         vmd_res = self.vmm.acquire_vm([GID1], self.ownername, 42, SANDBOX)

-         assert vmd.vm_name == vmd_res.vm_name

- 

-     def test_acquire_vm_extra_kwargs(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vmm.mark_server_start()

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd.store_field(self.rc, "state", VmStates.READY)

-         vmd.store_field(self.rc, "last_health_check", 2)

- 

-         kwargs = {

-             "task_id": "20-fedora-20-x86_64",

-             "build_id": "20",

-             "chroot": "fedora-20-x86_64"

-         }

-         vmd_got = self.vmm.acquire_vm([GID1], self.ownername, self.pid,

-                                       SANDBOX, **kwargs)

-         for k, v in kwargs.items():

-             assert vmd_got.get_field(self.rc, k) == v

- 

-     def test_another_owner_cannot_acquire_vm(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vmm.mark_server_start()

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd.store_field(self.rc, "state", VmStates.READY)

-         vmd.store_field(self.rc, "last_health_check", 2)

-         vmd.store_field(self.vmm.rc, "bound_to_user", "foo")

-         vmd.store_field(self.vmm.rc, "sandbox", SANDBOX)

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm(groups=[GID1], ownername=self.ownername,

-                     pid=self.pid, sandbox=SANDBOX)

-         vm = self.vmm.acquire_vm(groups=[GID1], ownername="foo", pid=self.pid,

-                                  sandbox=SANDBOX)

-         assert vm.vm_name == self.vm_name

- 

-     def test_different_sandbox_cannot_acquire_vm(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vmm.mark_server_start()

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd.store_field(self.rc, "state", VmStates.READY)

-         vmd.store_field(self.rc, "last_health_check", 2)

-         vmd.store_field(self.vmm.rc, "bound_to_user", "foo")

-         vmd.store_field(self.vmm.rc, "sandbox", "sandboxA")

- 

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm(groups=[GID1], ownername="foo",

-                     pid=self.pid, sandbox="sandboxB")

-         vm = self.vmm.acquire_vm(groups=[GID1], ownername="foo", pid=self.pid,

-                                  sandbox="sandboxA")

-         assert vm.vm_name == self.vm_name

- 

-     def test_acquire_vm(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vmm.mark_server_start()

- 

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd_alt = self.vmm.add_vm_to_pool(self.vm_ip, "vm_alt", GID1)

-         vmd2 = self.vmm.add_vm_to_pool(self.vm2_ip, self.vm2_name, GID2)

- 

-         vmd.store_field(self.rc, "state", VmStates.READY)

-         vmd_alt.store_field(self.rc, "state", VmStates.READY)

-         vmd2.store_field(self.rc, "state", VmStates.READY)

- 

-         vmd.store_field(self.rc, "last_health_check", 2)

-         vmd_alt.store_field(self.rc, "last_health_check", 2)

-         vmd2.store_field(self.rc, "last_health_check", 2)

- 

-         vmd_alt.store_field(self.vmm.rc, "bound_to_user", self.ownername)

-         vmd_alt.store_field(self.vmm.rc, "sandbox", SANDBOX)

- 

-         vmd_got_first = self.vmm.acquire_vm([GID1, GID2],

-                 ownername=self.ownername, pid=self.pid, sandbox=SANDBOX)

-         assert vmd_got_first.vm_name == "vm_alt"

- 

-         vmd_got_second = self.vmm.acquire_vm([GID1, GID2],

-                 ownername=self.ownername, pid=self.pid, sandbox=SANDBOX)

-         assert vmd_got_second.vm_name == self.vm_name

- 

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm(groups=[GID1], ownername=self.ownername,

-                     pid=self.pid, sandbox=SANDBOX)

- 

-         vmd_got_third = self.vmm.acquire_vm(groups=[GID1, GID2],

-                 ownername=self.ownername, pid=self.pid, sandbox=SANDBOX)

-         assert vmd_got_third.vm_name == self.vm2_name

- 

-     def test_acquire_vm_per_user_limit(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vmm.mark_server_start()

-         max_vm_per_user = self.opts.build_groups[GID1]["max_vm_per_user"]

- 

-         vmd_list = []

-         for idx in range(max_vm_per_user + 1):

-             vmd = self.vmm.add_vm_to_pool("127.0.{}.1".format(idx), "vm_{}".format(idx), GID1)

-             vmd.store_field(self.rc, "state", VmStates.READY)

-             vmd.store_field(self.rc, "last_health_check", 2)

-             vmd_list.append(vmd)

- 

-         for idx in range(max_vm_per_user):

-             self.vmm.acquire_vm([GID1], self.ownername, idx, SANDBOX)

- 

-         with pytest.raises(NoVmAvailable):

-             self.vmm.acquire_vm([GID1], self.ownername, 42, SANDBOX)

- 

-     def test_acquire_only_ready_state(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vmm.mark_server_start()

- 

-         vmd_main = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd_main.store_field(self.rc, "last_health_check", 2)

- 

-         for state in [VmStates.IN_USE, VmStates.GOT_IP, VmStates.CHECK_HEALTH,

-                       VmStates.TERMINATING, VmStates.CHECK_HEALTH_FAILED]:

-             vmd_main.store_field(self.rc, "state", state)

-             with pytest.raises(NoVmAvailable):

-                 self.vmm.acquire_vm(groups=[GID1], ownername=self.ownername,

-                                     pid=self.pid, sandbox=SANDBOX)

- 

-     def test_acquire_and_release_vm(self, mc_time):

-         mc_time.time.return_value = 0

-         self.vmm.mark_server_start()

- 

-         vmd_main = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd_alt = self.vmm.add_vm_to_pool(self.vm_ip, "vm_alt", GID1)

- 

-         vmd_main.store_field(self.rc, "state", VmStates.READY)

-         vmd_alt.store_field(self.rc, "state", VmStates.READY)

-         vmd_alt.store_field(self.vmm.rc, "bound_to_user", self.ownername)

-         vmd_alt.store_field(self.vmm.rc, "sandbox", SANDBOX)

-         vmd_main.store_field(self.rc, "last_health_check", 2)

-         vmd_alt.store_field(self.rc, "last_health_check", 2)

- 

-         vmd_got_first = self.vmm.acquire_vm(

-             groups=[GID1], ownername=self.ownername, pid=self.pid,

-             sandbox=SANDBOX)

-         assert vmd_got_first.vm_name == "vm_alt"

- 

-         self.vmm.release_vm("vm_alt")

-         vmd_got_again = self.vmm.acquire_vm(

-             groups=[GID1], ownername=self.ownername, pid=self.pid,

-             sandbox=SANDBOX)

-         assert vmd_got_again.vm_name == "vm_alt"

- 

-         vmd_got_another = self.vmm.acquire_vm(

-             groups=[GID1], ownername=self.ownername, pid=self.pid,

-             sandbox=SANDBOX)

-         assert vmd_got_another.vm_name == self.vm_name

- 

-     def test_release_only_in_use(self):

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

- 

-         for state in [VmStates.READY, VmStates.GOT_IP, VmStates.CHECK_HEALTH,

-                       VmStates.TERMINATING, VmStates.CHECK_HEALTH_FAILED]:

-             vmd.store_field(self.rc, "state", state)

- 

-             assert not self.vmm.release_vm(self.vm_name)

- 

-     def rcv_from_ps_message_bus(self):

-         # don't forget to subscribe self.ps

-         rcv_msg_list = []

-         for i in range(10):

-             msg = self.ps.get_message()

-             if msg:

-                 rcv_msg_list.append(msg)

-             time.sleep(0.01)

-         return rcv_msg_list

- 

-     def test_start_vm_termination(self):

-         self.ps = self.vmm.rc.pubsub(ignore_subscribe_messages=True)

-         self.ps.subscribe(PUBSUB_MB)

-         self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

- 

-         self.vmm.start_vm_termination(self.vm_name)

-         rcv_msg_list = self.rcv_from_ps_message_bus()

-         # print(rcv_msg_list)

-         assert len(rcv_msg_list) == 1

-         msg = rcv_msg_list[0]

-         assert msg["type"] == "message"

-         data = json.loads(msg["data"])

-         assert data["topic"] == EventTopics.VM_TERMINATION_REQUEST

-         assert data["vm_name"] == self.vm_name

- 

-     def test_start_vm_termination_2(self):

-         self.ps = self.vmm.rc.pubsub(ignore_subscribe_messages=True)

-         self.ps.subscribe(PUBSUB_MB)

- 

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd.store_field(self.rc, "state", VmStates.TERMINATING)

-         self.vmm.start_vm_termination(self.vm_name, allowed_pre_state=VmStates.TERMINATING)

-         rcv_msg_list = self.rcv_from_ps_message_bus()

-         # print(rcv_msg_list)

-         assert len(rcv_msg_list) == 1

-         msg = rcv_msg_list[0]

-         assert msg["type"] == "message"

-         data = json.loads(msg["data"])

-         assert data["topic"] == EventTopics.VM_TERMINATION_REQUEST

-         assert data["vm_name"] == self.vm_name

- 

-     def test_start_vm_termination_fail(self):

-         self.ps = self.vmm.rc.pubsub(ignore_subscribe_messages=True)

-         self.ps.subscribe(PUBSUB_MB)

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         vmd.store_field(self.rc, "state", VmStates.TERMINATING)

- 

-         self.vmm.start_vm_termination(self.vm_name)

-         rcv_msg_list = self.rcv_from_ps_message_bus()

-         assert len(rcv_msg_list) == 0

- 

-         vmd.store_field(self.rc, "state", VmStates.READY)

-         self.vmm.start_vm_termination(self.vm_name, allowed_pre_state=VmStates.IN_USE)

-         rcv_msg_list = self.rcv_from_ps_message_bus()

-         assert len(rcv_msg_list) == 0

-         assert vmd.get_field(self.rc, "state") == VmStates.READY

- 

-         vmd.store_field(self.rc, "state", VmStates.TERMINATING)

-         self.vmm.start_vm_termination(self.vm_name)

-         rcv_msg_list = self.rcv_from_ps_message_bus()

-         assert len(rcv_msg_list) == 0

-         assert vmd.get_field(self.rc, "state") == VmStates.TERMINATING

- 

-     def test_remove_vm_from_pool_only_terminated(self):

-         vmd = self.vmm.add_vm_to_pool(self.vm_ip, self.vm_name, GID1)

-         for state in [VmStates.IN_USE, VmStates.GOT_IP, VmStates.CHECK_HEALTH,

-                       VmStates.READY, VmStates.CHECK_HEALTH_FAILED]:

- 

-             vmd.store_field(self.vmm.rc, "state", state)

-             with pytest.raises(VmError):

-                 self.vmm.remove_vm_from_pool(self.vm_name)

- 

-         vmd.store_field(self.vmm.rc, "state", VmStates.TERMINATING)

-         self.vmm.remove_vm_from_pool(self.vm_name)

-         assert self.vmm.rc.scard(KEY_VM_POOL.format(group=GID1)) == 0

- 

-     def test_get_vms(self, capsys):

-         vmd_1 = self.vmm.add_vm_to_pool(self.vm_ip, "a1", GID1)

-         vmd_2 = self.vmm.add_vm_to_pool(self.vm_ip, "a2", GID1)

-         vmd_3 = self.vmm.add_vm_to_pool(self.vm_ip, "b1", 1)

-         vmd_4 = self.vmm.add_vm_to_pool(self.vm_ip, "b2", 1)

-         vmd_5 = self.vmm.add_vm_to_pool(self.vm_ip, "b3", 1)

- 

-         assert set(v.vm_name for v in self.vmm.get_all_vm_in_group(0)) == set(["a1", "a2"])

-         assert set(v.vm_name for v in self.vmm.get_all_vm_in_group(1)) == set(["b1", "b2", "b3"])

- 

-         assert set(v.vm_name for v in self.vmm.get_all_vm()) == set(["a1", "a2", "b1", "b2", "b3"])

- 

-         vmd_1.store_field(self.rc, "state", VmStates.GOT_IP)

-         vmd_2.store_field(self.rc, "state", VmStates.GOT_IP)

-         vmd_3.store_field(self.rc, "state", VmStates.GOT_IP)

-         vmd_4.store_field(self.rc, "state", VmStates.READY)

-         vmd_5.store_field(self.rc, "state", VmStates.IN_USE)

- 

-         vmd_list = self.vmm.get_vm_by_group_and_state_list(group=None, state_list=[VmStates.GOT_IP, VmStates.IN_USE])

-         assert set(v.vm_name for v in vmd_list) == set(["a1", "a2", "b1", "b3"])

-         vmd_list = self.vmm.get_vm_by_group_and_state_list(group=1, state_list=[VmStates.READY])

-         assert set(v.vm_name for v in vmd_list) == set(["b2"])

- 

-         self.vmm.info()

- 

-     def test_look_up_vms_by_ip(self, capsys):

-         self.vmm.add_vm_to_pool(self.vm_ip, "a1", GID1)

-         r1 = self.vmm.lookup_vms_by_ip(self.vm_ip)

-         assert len(r1) == 1

-         assert r1[0].vm_name == "a1"

- 

-         self.vmm.add_vm_to_pool(self.vm_ip, "a2", GID1)

-         r2 = self.vmm.lookup_vms_by_ip(self.vm_ip)

-         assert len(r2) == 2

-         r2 = sorted(r2, key=lambda vmd: vmd.vm_name)

-         assert r2[0].vm_name == "a1"

-         assert r2[1].vm_name == "a2"

- 

-         self.vmm.add_vm_to_pool("127.1.1.111", "b1", 1)

- 

-         r3 = self.vmm.lookup_vms_by_ip(self.vm_ip)

-         assert len(r3) == 2

-         r3 = sorted(r3, key=lambda vmd: vmd.vm_name)

-         assert r3[0].vm_name == "a1"

-         assert r3[1].vm_name == "a2"

- 

-     def test_mark_server_start(self, mc_time):

-         assert self.rc.hget(KEY_SERVER_INFO, "server_start_timestamp") is None

-         for i in range(100):

-             val = 100 * i + 0.12345

-             mc_time.time.return_value = val

-             self.vmm.mark_server_start()

-             assert self.rc.hget(KEY_SERVER_INFO, "server_start_timestamp") == "{}".format(val)

@@ -1,227 +0,0 @@

- # coding: utf-8

- import shutil

- import tempfile

- import time

- from multiprocessing import Queue

- import types

- 

- from munch import Munch

- from redis import ConnectionError

- from copr_backend.exceptions import CoprSpawnFailError

- 

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage.spawn import Spawner, spawn_instance, do_spawn_and_publish

- 

- from unittest import mock, skip

- from unittest.mock import MagicMock

- import pytest

- 

- 

- """

- REQUIRES RUNNING REDIS

- TODO: look if https://github.com/locationlabs/mockredis can be used

- """

- 

- MODULE_REF = "copr_backend.vm_manage.spawn"

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_spawn_instance():

-     with mock.patch("{}.spawn_instance".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_process():

-     with mock.patch("{}.Process".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_run_ans():

-     with mock.patch("{}.run_ansible_playbook_cli".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_spawn_instance():

-     with mock.patch("{}.spawn_instance".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_grc():

-     with mock.patch("{}.get_redis_connection".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- class TestSpawner(object):

- 

-     def setup_method(self, method):

-         self.test_root_path = tempfile.mkdtemp()

-         self.spawn_pb_path = "{}/spawn.yml".format(self.test_root_path)

-         self.opts = Munch(

-             redis_db=9,

-             redis_port=7777,

-             ssh=Munch(

-                 transport="ssh"

-             ),

-             build_groups={

-                 0: {

-                     "spawn_playbook": self.spawn_pb_path,

-                     "name": "base",

-                     "archs": ["i386", "x86_64"]

-                 }

-             },

- 

-             fedmsg_enabled=False,

-             sleeptime=0.1,

-             do_sign=True,

-             timeout=1800,

-             # destdir=self.tmp_dir_path,

-             results_baseurl="/tmp",

-         )

-         self.try_spawn_args = '-c ssh {}'.format(self.spawn_pb_path)

- 

-         self.grl_patcher = mock.patch("{}.get_redis_logger".format(MODULE_REF))

-         self.grl_patcher.start()

- 

-         self.checker = MagicMock()

-         self.terminator = MagicMock()

- 

-         self.spawner = Spawner(self.opts)

-         self.spawner.recycle = types.MethodType(mock.MagicMock, self.spawner)

-         self.vm_ip = "127.0.0.1"

-         self.vm_name = "localhost"

-         self.group = 0

-         self.username = "bob"

- 

-         self.rc = get_redis_connection(self.opts)

- 

-         self.logger = MagicMock()

- 

-     def teardown_method(self, method):

-         self.grl_patcher.stop()

-         shutil.rmtree(self.test_root_path)

-         keys = self.rc.keys("*")

-         if keys:

-             self.rc.delete(*keys)

- 

-     def touch_pb(self):

-         with open(self.spawn_pb_path, "w") as handle:

-             handle.write("foobar")

- 

-     # def test_start_spawn(self, mc_spawn_instance, mc_process):

-     #     mc_spawn_instance.return_value = {"vm_name": self.vm_name, "ip": self.vm_ip}

-     #

-     #     # undefined group

-     #     with pytest.raises(CoprSpawnFailError):

-     #         self.spawner.start_spawn(1)

-     #

-     #     # missing playbook

-     #     with pytest.raises(CoprSpawnFailError):

-     #         self.spawner.start_spawn(0)

-     #

-     #     # None playbook

-     #     self.opts.build_groups[0]["spawn_playbook"] = None

-     #     with pytest.raises(CoprSpawnFailError):

-     #         self.spawner.start_spawn(0)

-     #

-     #     self.opts.build_groups[0]["spawn_playbook"] = self.spawn_pb_path

-     #     self.touch_pb()

-     #

-     #     p1 = mock.MagicMock()

-     #     mc_process.return_value = p1

-     #

-     #     self.spawner.start_spawn(0)

-     #     assert mc_process.called

-     #     assert self.spawner.child_processes == [p1]

-     #     assert p1.start.called

- 

-     def test_spawn_no_result(self, mc_run_ans):

-         self.touch_pb()

-         mc_run_ans.return_value = None

-         with pytest.raises(CoprSpawnFailError):

-             spawn_instance(self.spawn_pb_path, self.logger)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_spawn_ansible_call_error(self, mc_run_ans):

-         self.touch_pb()

-         mc_run_ans.side_effect = Exception("foobar")

-         with pytest.raises(CoprSpawnFailError) as err:

-             spawn_instance(self.spawn_pb_path, self.logger)

- 

-         assert "Error during ansible invocation" in err.value.msg

- 

-     def test_spawn_no_ip_in_result(self, mc_run_ans):

-         self.touch_pb()

-         mc_run_ans.return_value = "foobar"

-         with pytest.raises(CoprSpawnFailError) as err:

-             spawn_instance(self.spawn_pb_path, self.logger)

- 

-         assert "No ip in the result" in err.value.msg

- 

-     def test_spawn_bad_ip(self, mc_run_ans):

-         self.touch_pb()

-         mc_run_ans.return_value = "\"IP=foobar\"  \"vm_name=foobar\""

-         with pytest.raises(CoprSpawnFailError) as err:

-             spawn_instance(self.spawn_pb_path, self.logger)

- 

-         assert "Invalid IP" in err.value.msg

- 

-         for bad_ip in ["256.0.0.2", "not-an-ip", "example.com", ""]:

-             mc_run_ans.return_value = "\"IP={}\" \"vm_name=foobar\"".format(bad_ip)

-             with pytest.raises(CoprSpawnFailError) as err:

-                 spawn_instance(self.spawn_pb_path, self.logger)

- 

-     def test_spawn_no_vm_name(self, mc_run_ans):

-         self.touch_pb()

-         mc_run_ans.return_value = "\"IP=foobar\""

-         with pytest.raises(CoprSpawnFailError) as err:

-             spawn_instance(self.spawn_pb_path, self.logger)

- 

-         assert "No vm_name" in err.value.msg

- 

-     def test_spawn_ok(self, mc_run_ans):

-         self.touch_pb()

-         mc_run_ans.return_value = " \"IP=127.0.0.1\" \"vm_name=foobar\""

- 

-         result = spawn_instance(self.spawn_pb_path, self.logger)

-         assert result == {'vm_ip': '127.0.0.1', 'vm_name': 'foobar'}

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_do_spawn_and_publish_copr_spawn_error(self, mc_spawn_instance, mc_grc):

-         mc_spawn_instance.side_effect = CoprSpawnFailError("foobar")

-         result = do_spawn_and_publish(self.opts, self.spawn_pb_path, self.group)

-         assert result is None

-         assert not mc_grc.called

- 

-     def test_do_spawn_and_publish_any_spawn_error(self, mc_spawn_instance, mc_grc):

-         mc_spawn_instance.side_effect = OSError("foobar")

-         result = do_spawn_and_publish(self.opts, self.spawn_pb_path, self.group)

-         assert result is None

-         assert not mc_grc.called

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_do_spawn_and_publish_ok(self, mc_spawn_instance, mc_grc):

-         mc_rc = mock.MagicMock()

-         mc_grc.return_value = mc_rc

-         mc_spawn_instance.return_value = {"result": "foobar"}

- 

-         do_spawn_and_publish(self.opts, self.spawn_pb_path, self.group)

-         assert mc_grc.called

-         assert mc_rc.publish.called

-         assert mc_rc.publish.call_args == mock.call(

-             'copr:backend:vm:pubsub::',

-             '{"topic": "vm_spawned", "group": 0, "result": "foobar"}')

- 

-     def test_do_spawn_and_publish_publish_error(self, mc_spawn_instance, mc_grc):

-         mc_spawn_instance.return_value = {"result": "foobar"}

-         mc_grc.side_effect = ConnectionError()

- 

-         do_spawn_and_publish(self.opts, self.spawn_pb_path, self.group)

-         assert mc_grc.called

@@ -1,187 +0,0 @@

- # coding: utf-8

- import shutil

- from subprocess import CalledProcessError

- import tempfile

- import time

- from multiprocessing import Queue

- import types

- 

- from munch import Munch

- from redis import ConnectionError

- from copr_backend.exceptions import CoprSpawnFailError

- 

- from copr_backend.helpers import get_redis_connection

- from copr_backend.vm_manage import EventTopics

- from copr_backend.vm_manage.terminate import Terminator, terminate_vm

- 

- from unittest import mock, skip

- from unittest.mock import MagicMock

- import pytest

- 

- 

- """

- REQUIRES RUNNING REDIS

- TODO: look if https://github.com/locationlabs/mockredis can be used

- """

- 

- MODULE_REF = "copr_backend.vm_manage.terminate"

- 

- @pytest.yield_fixture

- def mc_time():

-     with mock.patch("{}.time".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_terminate_vm():

-     with mock.patch("{}.terminate_vm".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_process():

-     with mock.patch("{}.Process".format(MODULE_REF)) as handle:

-         yield handle

- 

- @pytest.yield_fixture

- def mc_run_ans():

-     with mock.patch("{}.run_ansible_playbook_cli".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_spawn_instance():

-     with mock.patch("{}.spawn_instance".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- @pytest.yield_fixture

- def mc_grc():

-     with mock.patch("{}.get_redis_connection".format(MODULE_REF)) as handle:

-         yield handle

- 

- 

- class TestTerminate(object):

- 

-     def setup_method(self, method):

-         self.test_root_path = tempfile.mkdtemp()

-         self.terminate_pb_path = "{}/terminate.yml".format(self.test_root_path)

-         self.opts = Munch(

-             redis_port=7777,

-             ssh=Munch(

-                 transport="ssh"

-             ),

-             build_groups={

-                 0: {

-                     "terminate_playbook": self.terminate_pb_path,

-                     "name": "base",

-                     "archs": ["i386", "x86_64"],

-                 }

-             },

- 

-             fedmsg_enabled=False,

-             sleeptime=0.1,

-             do_sign=True,

-             timeout=1800,

-             # destdir=self.tmp_dir_path,

-             results_baseurl="/tmp",

-         )

-         self.grl_patcher = mock.patch("{}.get_redis_logger".format(MODULE_REF))

-         self.grl_patcher.start()

-         # self.try_spawn_args = '-c ssh {}'.format(self.spawn_pb_path)

- 

-         # self.callback = TestCallback()

-         self.checker = MagicMock()

-         self.terminator = MagicMock()

- 

-         self.terminator = Terminator(self.opts)

-         self.terminator.recycle = types.MethodType(mock.MagicMock, self.terminator)

-         self.vm_ip = "127.0.0.1"

-         self.vm_name = "localhost"

-         self.group = 0

-         self.username = "bob"

- 

-         self.rc = get_redis_connection(self.opts)

-         self.log_msg_list = []

- 

-         self.logger = MagicMock()

- 

-     def teardown_method(self, method):

-         shutil.rmtree(self.test_root_path)

-         keys = self.rc.keys("*")

-         if keys:

-             self.rc.delete(*keys)

- 

-         self.grl_patcher.stop()

- 

-     def touch_pb(self):

-         with open(self.terminate_pb_path, "w") as handle:

-             handle.write("foobar")

- 

-     # def test_start_terminate(self, mc_process):

-     #     # mc_spawn_instance.return_value = {"vm_name": self.vm_name, "ip": self.vm_ip}

-     #

-     #     # undefined group

-     #     with pytest.raises(CoprSpawnFailError):

-     #         self.terminator.terminate_vm(group=1, vm_name=self.vm_name, vm_ip=self.vm_ip)

-     #

-     #     # missing playbook

-     #     with pytest.raises(CoprSpawnFailError):

-     #         self.terminator.terminate_vm(group=0, vm_name=self.vm_name, vm_ip=self.vm_ip)

-     #

-     #     # None playbook

-     #     self.opts.build_groups[0]["terminate_playbook"] = None

-     #     with pytest.raises(CoprSpawnFailError):

-     #         self.terminator.terminate_vm(group=0, vm_name=self.vm_name, vm_ip=self.vm_ip)

-     #

-     #     self.opts.build_groups[0]["terminate_playbook"] = self.terminate_pb_path

-     #     self.touch_pb()

-     #

-     #     p1 = mock.MagicMock()

-     #     mc_process.return_value = p1

-     #

-     #     self.terminator.terminate_vm(group=0, vm_name=self.vm_name, vm_ip=self.vm_ip)

-     #     assert mc_process.called

-     #     assert self.terminator.child_processes == [p1]

-     #     assert p1.start.called

- 

-     def test_terminate_vm_on_error(self, mc_run_ans):

-         mc_run_ans.side_effect = CalledProcessError(0, cmd=["ls"])

- 

-         # doesn't raise an error

-         terminate_vm(self.opts, self.terminate_pb_path, 0, self.vm_name, self.vm_ip)

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_do_spawn_and_publish_ok(self, mc_run_ans, mc_grc):

-         mc_rc = mock.MagicMock()

-         mc_grc.return_value = mc_rc

- 

-         terminate_vm(self.opts, self.terminate_pb_path, 0, self.vm_name, self.vm_ip)

- 

-         assert mc_run_ans.called

-         expected_cmd = '-c ssh {} --extra-vars=\'{{"copr_task": {{"vm_name": "{}", "ip": "{}"}}}}\''.format(

-             self.terminate_pb_path, self.vm_name, self.vm_ip)

- 

-         assert expected_cmd == mc_run_ans.call_args[:-1][0][0]

- 

-         assert mc_grc.called

-         assert mc_rc.publish.called

- 

-         expected_call = mock.call(

-             'copr:backend:vm:pubsub::',

-             '{"vm_ip": "127.0.0.1", "vm_name": "localhost", '

-             '"topic": "vm_terminated", "group": 0, "result": "OK"}')

-         assert mc_rc.publish.call_args == expected_call

- 

-     @skip("Fixme or remove, test doesn't work.")

-     def test_do_spawn_and_publish_error(self, mc_run_ans, mc_grc):

-         mc_grc.side_effect = ConnectionError()

- 

-         terminate_vm(self.opts, self.terminate_pb_path, 0, self.vm_name, self.vm_ip)

- 

-         assert mc_run_ans.called

-         expected_cmd = '-c ssh {} --extra-vars=\'{{"copr_task": {{"vm_name": "{}", "ip": "{}"}}}}\''.format(

-             self.terminate_pb_path, self.vm_name, self.vm_ip)

- 

-         assert expected_cmd == mc_run_ans.call_args[:-1][0][0]

-         assert mc_grc.called

@@ -2,7 +2,7 @@

  Description=Copr Backend service, Log Handler component

  After=syslog.target network.target auditd.service

  PartOf=copr-backend.target

- Before=copr-backend-vmm.service copr-backend-build.service copr-backend-action.service

+ Before=copr-backend-build.service copr-backend-action.service

  Wants=logrotate.timer

  

  [Service]
@@ -14,4 +14,4 @@

  

  [Install]

  WantedBy=multi-user.target

- RequiredBy=copr-backend.target copr-backend-vmm.service copr-backend-build.service copr-backend-action.service

+ RequiredBy=copr-backend.target copr-backend-build.service copr-backend-action.service

@@ -1,15 +0,0 @@

- [Unit]

- Description=Copr Backend service, Virtual Machine Management component

- After=syslog.target network.target auditd.service

- PartOf=copr-backend.target

- Wants=logrotate.timer

- 

- [Service]

- Type=simple

- User=copr

- Group=copr

- ExecStart=/usr/bin/copr_run_vmm.py

- Restart=on-failure

- 

- [Install]

- WantedBy=multi-user.target

@@ -1,7 +1,7 @@

  [Unit]

  Description=Copr Backend service

  After=syslog.target network.target auditd.service

- Requires=copr-backend-vmm.service copr-backend-log.service copr-backend-build.service copr-backend-action.service

+ Requires=copr-backend-log.service copr-backend-build.service copr-backend-action.service

  Wants=logrotate.timer

  

  [Install]

@@ -3,6 +3,8 @@

  

  from six import with_metaclass

  

+ # We don't know how to define the enums without `class`.

+ # pylint: disable=too-few-public-methods

  

  class EnumType(type):

      def _wrap(cls, attr=None):

@@ -101,6 +101,7 @@

  BuildRequires: python3-pygments

  BuildRequires: python3-pylibravatar

  BuildRequires: python3-pytest

+ BuildRequires: python3-pytest-cov

  BuildRequires: python3-pytz

  BuildRequires: python3-redis

  BuildRequires: python3-requests

@@ -949,8 +949,12 @@

      def id_fixed_width(self):

          return "{:08d}".format(self.id)

  

-     def get_import_log_urls(self, admin=False):

-         logs = [self.import_log_url_backend]

+     def get_source_log_urls(self, admin=False):

+         """

+         Return a list of URLs to important build _source_ logs.  The list is

+         changing as the state of build is changing.

+         """

+         logs = [self.source_live_log_url, self.source_backend_log_url]

          if admin:

              logs.append(self.import_log_url_distgit)

          return list(filter(None, logs))
@@ -963,14 +967,48 @@

          return None

  

      @property

-     def import_log_url_backend(self):

-         parts = ["results", self.copr.owner_name, self.copr_dirname,

-                  "srpm-builds", self.id_fixed_width,

-                  "builder-live.log" if self.source_status == StatusEnum("running")

-                                     else "builder-live.log.gz"]

+     def result_dir_url(self):

+         """

+         URL for the result-directory on backend (the source/SRPM build).

+         """

+         if not self.result_dir:

+             return None

+         parts = [

+             "results", self.copr.owner_name, self.copr_dirname,

+             # TODO: we should use self.result_dir instead of id_fixed_width

+             "srpm-builds", self.id_fixed_width,

+         ]

          path = os.path.normpath(os.path.join(*parts))

          return urljoin(app.config["BACKEND_BASE_URL"], path)

  

+     def _compressed_log_variant(self, basename, states_raw_log):

+         if not self.result_dir:

+             return None

+         if self.source_state in states_raw_log:

+             return "/".join([self.result_dir_url, basename])

+         if self.source_state in ["failed", "succeeded", "canceled",

+                                  "importing"]:

+             return "/".join([self.result_dir_url, basename + ".gz"])

+         return None

+ 

+     @property

+     def source_live_log_url(self):

+         """

+         Full URL to the builder-live.log(.gz) for the source (SRPM) build.

+         """

+         return self._compressed_log_variant(

+             "builder-live.log", ["running"]

+         )

+ 

+     @property

+     def source_backend_log_url(self):

+         """

+         Full URL to the builder-live.log(.gz) for the source (SRPM) build.

+         """

+         return self._compressed_log_variant(

+             "backend.log", ["starting", "running"]

+         )

+ 

      @property

      def source_json_dict(self):

          if not self.source_json:
@@ -1049,6 +1087,13 @@

          return {b.name: b for b in self.build_chroots}

  

      @property

+     def source_state(self):

+         """

+         Return text representation of status of this build

+         """

+         return StatusEnum(self.source_status)

+ 

+     @property

      def status(self):

          """

          Return build status.
@@ -1509,19 +1554,42 @@

          return urljoin(app.config["BACKEND_BASE_URL"], os.path.join(

              "results", self.build.copr_dir.full_name, self.name, self.result_dir, ""))

  

-     @property

-     def live_log_link(self):

+     def _compressed_log_variant(self, basename, states_raw_log):

+         if not self.result_dir:

+             return None

          if not self.build.package:

+             # no source build done, yet

              return None

+         if self.state in states_raw_log:

+             return os.path.join(self.result_dir_url,

+                                 basename)

+         if self.state in ["failed", "succeeded", "canceled", "importing"]:

+             return os.path.join(self.result_dir_url,

+                                 basename + ".gz")

+         return None

  

-         if not (self.finished or self.state == "running"):

-             return None

+     @property

+     def rpm_live_log_url(self):

+         """ Full URL to the builder-live.log.gz for RPM build.  """

+         return self._compressed_log_variant("builder-live.log", ["running"])

  

-         if not self.result_dir_url:

-             return None

+     @property

+     def rpm_backend_log_url(self):

+         """ Link to backend.log[.gz] related to RPM build.  """

+         return self._compressed_log_variant("backend.log",

+                                             ["starting", "running"])

  

-         return os.path.join(self.result_dir_url,

-                             "builder-live.log" if self.state == 'running' else "builder-live.log.gz")

+     @property

+     def rpm_live_logs(self):

+         """ return list of live log URLs """

+         logs = []

+         log = self.rpm_backend_log_url

+         if log:

+             logs.append(log)

+         log = self.rpm_live_log_url

+         if log:

+             logs.append(log)

+         return logs

  

  

  class LegalFlag(db.Model, helpers.Serializer):

@@ -173,9 +173,11 @@

        <div class="panel-body">

          <dl class="dl-horizontal">

          {{ describe_failure(build) }}

-         <dt>SRPM build log:</dt>

+         <dt>Source state:</dt>

+             <dd>{{ build_state_text(build.source_state) }} </dd>

+         <dt>Source build log:</dt>

          {% if build.source_status | state_from_num in ['pending', 'starting'] %}

-           <dd> Build has not started yet</dd>

+           <dd> Source build has not started yet</dd>

          {% else %}

            {% if build.resubmitted_from_id and build.source_is_uploaded %}

              <dd>Build resubmitted from build
@@ -186,7 +188,7 @@

                {% endif %}

              </dd>

            {% else %}

-             {% for url in build.get_import_log_urls(g.user.admin) %}

+             {% for url in build.get_source_log_urls(g.user.admin) %}

                <dd>

                  <a href="{{ url }}">{{ url | basename }}</a>

                  {{ "," if not loop.last }}
@@ -216,6 +218,7 @@

                <th>Chroot Name</th>

                <th>Dist Git Source</th>

                <th>Build Time</th>

+               <th>Logs</th>

                <th>State</th>

              </tr>

            </thead>
@@ -248,11 +251,15 @@

                  {{ chroot.started_on|time_ago(chroot.ended_on) }}

                </td>

                <td>

-                 {% if chroot.live_log_link %}

-                     <a href="{{ chroot.live_log_link }}">{{ build_state_text(chroot.state) }}</a>

+                 {% for log in chroot.rpm_live_logs %}

+                 <a href="{{ log }}"> {{ log | basename }} </a>

+                 {{ "," if not loop.last }}

                  {% else %}

-                   {{ build_state_text(chroot.state) }}

-                 {% endif %}

+                 RPM build has not started yet

+                 {% endfor %}

+               </td>

+               <td>

+                 {{ build_state_text(chroot.state) }}

                </td>

              </tr>

            {% endfor %}

@@ -35,7 +35,7 @@

  def to_source_chroot(build):

      return {

          "state": StatusEnum(build.source_status),

-         "result_url": os.path.dirname(build.import_log_url_backend),

+         "result_url": os.path.dirname(build.source_live_log_url),

          #  @TODO Do we have such information stored?

          # "started_on": None,

          # "ended_on": None

@@ -1,5 +1,7 @@

  import pytest

  

+ import coprs

+ 

  from copr_common.enums import StatusEnum

  from tests.coprs_test_case import CoprsTestCase

  
@@ -87,3 +89,96 @@

          self.b1.canceled = False

          self.b1.source_status = StatusEnum("canceled")

          assert bch.finished

+ 

+     @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_builds",

+                              "f_db")

+     def test_build_logs(self):

+         config = coprs.app.config

+         config["COPR_DIST_GIT_LOGS_URL"] = "http://example-dist-git/url"

+ 

+         # no matter state,  result_dir none implies log None

+         self.b1.result_dir = None

+         assert self.b1.source_live_log_url is None

+         assert self.b1.source_backend_log_url is None

+ 

+         def _pfxd(basename):

+             pfx = ("http://copr-be-dev.cloud.fedoraproject.org/results/"

+                    "user1/foocopr/srpm-builds/00000001")

+             return "/".join([pfx, basename])

+ 

+         # pending state

+         self.b1.source_status = StatusEnum("pending")

+         assert self.b1.source_live_log_url is None

+         assert self.b1.source_backend_log_url is None

+ 

+         # starting state

+         self.b1.result_dir = "001"

+         self.b1.source_status = StatusEnum("starting")

+         assert self.b1.source_live_log_url is None

+         assert self.b1.source_backend_log_url == _pfxd("backend.log")

+ 

+         # running state

+         self.b1.source_status = StatusEnum("running")

+         assert self.b1.source_live_log_url == _pfxd("builder-live.log")

+         assert self.b1.source_backend_log_url == _pfxd("backend.log")

+ 

+         # importing state

+         self.b1.source_status = StatusEnum("importing")

+         assert self.b1.get_source_log_urls(admin=True) == [

+             _pfxd("builder-live.log.gz"),

+             _pfxd("backend.log.gz"),

+             "http://example-dist-git/url/1.log",

+         ]

+ 

+         for state in ["failed", "succeeded", "canceled", "importing"]:

+             self.b1.source_status = StatusEnum(state)

+             assert self.b1.source_live_log_url == _pfxd("builder-live.log.gz")

+             assert self.b1.source_backend_log_url == _pfxd("backend.log.gz")

+ 

+         for state in ["skipped", "forked", "waiting", "unknown"]:

+             self.b1.source_status = StatusEnum(state)

+             assert self.b1.source_live_log_url is None

+             assert self.b1.source_backend_log_url is None

+ 

+     @pytest.mark.usefixtures("f_users", "f_coprs", "f_mock_chroots", "f_builds",

+                              "f_db")

+     def test_buildchroot_logs(self):

+         build = self.b1_bc[0]

+ 

+         # no matter state,  result_dir none implies log None

+         build.result_dir = None

+         assert build.rpm_live_log_url is None

+         assert build.rpm_backend_log_url is None

+ 

+         def _pfxd(basename):

+             pfx = ("http://copr-be-dev.cloud.fedoraproject.org/results/"

+                    "user1/foocopr/fedora-18-x86_64/bar")

+             return "/".join([pfx, basename])

+ 

+         # pending state

+         build.status = StatusEnum("pending")

+         assert build.rpm_live_log_url is None

+         assert build.rpm_backend_log_url is None

+ 

+         # starting state

+         build.result_dir = "bar"

+         build.status = StatusEnum("starting")

+         assert build.rpm_live_log_url is None

+         assert build.rpm_backend_log_url == _pfxd("backend.log")

+ 

+         # running state

+         build.status = StatusEnum("running")

+         assert build.rpm_live_logs == [

+             _pfxd("backend.log"),

+             _pfxd("builder-live.log"),

+         ]

+ 

+         for state in ["failed", "succeeded", "canceled"]:

+             build.status = StatusEnum(state)

+             assert build.rpm_live_log_url == _pfxd("builder-live.log.gz")

+             assert build.rpm_backend_log_url == _pfxd("backend.log.gz")

+ 

+         for state in ["skipped", "forked", "waiting", "unknown"]:

+             build.status = StatusEnum(state)

+             assert build.rpm_live_log_url is None

+             assert build.rpm_backend_log_url is None

file modified
+3 -1
@@ -15,9 +15,11 @@

  

  ./build_aux/check-alembic-revisions

  

+ COVPARAMS=(--cov-report term-missing --cov run --cov coprs)

+ 

  common_path=$(readlink -f ../common)

  export PYTHONPATH="${PYTHONPATH+$PYTHONPATH:}$common_path"

  export COPR_CONFIG="$(pwd)/coprs_frontend/config/copr_unit_test.conf"

  

  cd coprs_frontend

- ./manage.py test "$@"

+ ./manage.py test "$@" "${COVPARAMS[@]}"

@@ -0,0 +1,42 @@

+ #! /bin/bash

+ 

+ # Cleanup Copr builder machine before re-using it for other build(s)

+ #

+ # This is executed when the builder VM is released from ticket by Resalloc

+ # server, before it can be re-assigned to other ticket.  Any non-zero exit

+ # status here makes the VM non-reusable (VM will be shut-down).  Concrete

+ # deployments can also run this script right after the VM is started.

+ #

+ # Copyright (C) 2020 Red Hat, Inc.

+ #

+ # This program is free software; you can redistribute it and/or modify

+ # it under the terms of the GNU General Public License as published by

+ # the Free Software Foundation; either version 2 of the License, or

+ # (at your option) any later version.

+ #

+ # This program is distributed in the hope that it will be useful,

+ # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

+ # GNU General Public License for more details.

+ #

+ # You should have received a copy of the GNU General Public License along

+ # with this program; if not, write to the Free Software Foundation, Inc.,

+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

+ 

+ set -x

+ set -e

+ 

+ # We need to be root! https://pagure.io/copr/copr/issue/1258

+ 

+ test "$UID" -eq 0

+ 

+ su - mockbuilder -c "/usr/bin/copr-rpmbuild-cancel"

+ rm -f /var/lib/copr-rpmbuild/pid

+ rm -f /var/lib/copr-rpmbuild/main.log

+ rm -rf /var/lib/copr-rpmbuild/results

+ 

+ shopt -s nullglob

+ set -- /etc/copr-builder/hooks/cleanup/*.sh

+ for arg; do

+     source "$arg"

+ done

@@ -0,0 +1,81 @@

+ #! /usr/bin/python

+ 

+ # Copyright (C) 2018 Red Hat, Inc.

+ 

+ # This program is free software; you can redistribute it and/or modify

+ # it under the terms of the GNU General Public License as published by

+ # the Free Software Foundation; either version 2 of the License, or

+ # (at your option) any later version.

+ #

+ # This program is distributed in the hope that it will be useful,

+ # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

+ # GNU General Public License for more details.

+ #

+ # You should have received a copy of the GNU General Public License along

+ # with this program; if not, write to the Free Software Foundation, Inc.,

+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

+ 

+ """

+ Cancel the background copr-rpmbuild process

+ 

+ Attempt (best effort) to cancel the background running process.  The Copr

+ backend code doesn't expect us to fail.

+ """

+ 

+ import logging

+ import os

+ import signal

+ import time

+ 

+ WORKDIR = "/var/lib/copr-rpmbuild"

+ PIDFILE = os.path.join(WORKDIR, "pid")

+ INTERRUPT_TIMEOUT = 30

+ INTERRUPT_SLEEP = 3

+ 

+ 

+ def _get_stderr_logger():

+     log = logging.getLogger(__name__)

+     log.setLevel(logging.INFO)

+     handler = logging.StreamHandler()

+     handler.setFormatter(logging.Formatter("%(message)s"))

+     log.addHandler(handler)

+     return log

+ 

+ LOG = _get_stderr_logger()

+ 

+ def _is_running(pid):

+     try:

+         os.kill(pid, 0)

+     except OSError:

+         # process not found, already shut-down

+         return False

+     return True

+ 

+ def _send_signal(pid, signum):

+     try:

+         os.kill(pid, signum)

+     except OSError as err:

+         LOG.warning("can't interrupt: %s (sig=%s)", err, signum)

+ 

+ def _main():

+     with open(PIDFILE, "r") as pidfd:

+         pid = int(pidfd.read().strip())

+ 

+     start = time.time()

+     LOG.debug("Trying to stop pid %s", pid)

+     found = False

+     while _is_running(pid):

+         found = True

+         _send_signal(pid, signal.SIGINT)

+         time.sleep(INTERRUPT_SLEEP)

+         if time.time() - start > INTERRUPT_TIMEOUT:

+             # last resort

+             _send_signal(pid, signal.SIGKILL)

+     print("success" if found else "PID {} not found".format(pid))

+ 

+ if __name__ == "__main__":

+     try:

+         _main()

+     except Exception:  # pylint: disable=broad-except

+         LOG.exception("Unexpected exception")

@@ -0,0 +1,73 @@

+ #! /usr/bin/python

+ 

+ # Copyright (C) 2018 Red Hat, Inc.

+ 

+ # This program is free software; you can redistribute it and/or modify

+ # it under the terms of the GNU General Public License as published by

+ # the Free Software Foundation; either version 2 of the License, or

+ # (at your option) any later version.

+ #

+ # This program is distributed in the hope that it will be useful,

+ # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

+ # GNU General Public License for more details.

+ #

+ # You should have received a copy of the GNU General Public License along

+ # with this program; if not, write to the Free Software Foundation, Inc.,

+ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

+ 

+ """

+ Print the log of copr-rpmbuild process to stdout

+ 

+ This script always succeeds (exit status 0), as long as the copr-rpmbuild

+ process was at all started before.  If the copr-rpmbuild is still running, the

+ script keeps waiting for the process to end and keeps appending the output to

+ stdout.

+ """

+ 

+ import logging

+ import os

+ import sys

+ import time

+ 

+ WORKDIR = "/var/lib/copr-rpmbuild"

+ LIVE_LOG = os.path.join(WORKDIR, "main.log")

+ PIDFILE = os.path.join(WORKDIR, "pid")

+ MAX_WAIT_FOR_RPMBUILD = 120

+ 

+ def _get_stdout_logger():

+     log = logging.getLogger(__name__)

+     handler = logging.StreamHandler(sys.stdout)

+     handler.setFormatter(logging.Formatter("%(message)s"))

+     log.addHandler(log)

+     return log

+ 

+ LOG = _get_stdout_logger()

+ 

+ def _tail_log():

+     for fname in [PIDFILE, LIVE_LOG]:

+         if not os.path.exists(fname):

+             LOG.warning("File %s doesn't exist, yet", fname)

+             return

+ 

+     with open(PIDFILE, "r") as pidfd:

+         pid = int(pidfd.read().strip())

+ 

+     tail = "/usr/bin/tail"

+     args = [

+         tail, "-F", "-n", "+0",

+         "--pid={}".format(pid),

+         LIVE_LOG,

+     ]

+     os.execv(tail, args)

+ 

+ def _main():

+     start = time.time()

+     while True:

+         _tail_log()

+         if time.time() - start > MAX_WAIT_FOR_RPMBUILD:

+             LOG.fatal("Unable to wait for copr-rpmbuild process")

+         time.sleep(5)

+ 

+ if __name__ == "__main__":

+     _main()

@@ -0,0 +1,2 @@

+ This directory contains *.sh files being executed (sourced) by

+ /usr/bin/copr-builder-cleanup script.

file modified
+25 -3
@@ -18,7 +18,7 @@

  %{expand: %%global latest_requires_packages %1 %%{?latest_requires_packages}}

  

  Name:    copr-rpmbuild

- Version: 0.38

+ Version: 0.39

  Summary: Run COPR build tasks

  Release: 1%{?dist}

  URL: https://pagure.io/copr/copr
@@ -193,7 +193,6 @@

  

  install -d %{buildroot}%{_bindir}

  install -m 755 main.py %{buildroot}%{_bindir}/copr-rpmbuild

- sed -i '1 s|#.*|#! /usr/bin/%python|' %{buildroot}%{_bindir}/copr-rpmbuild

  install -m 644 main.ini %{buildroot}%{_sysconfdir}/copr-rpmbuild/main.ini

  install -m 644 mock.cfg.j2 %{buildroot}%{_sysconfdir}/copr-rpmbuild/mock.cfg.j2

  install -m 644 rpkg.conf.j2 %{buildroot}%{_sysconfdir}/copr-rpmbuild/rpkg.conf.j2
@@ -208,12 +207,28 @@

  

  install -d %{buildroot}%{_mandir}/man1

  install -p -m 644 man/copr-rpmbuild.1 %{buildroot}/%{_mandir}/man1/

+ install -p -m 755 bin/copr-builder-cleanup %buildroot%_bindir

  install -p -m 755 bin/copr-sources-custom %buildroot%_bindir

+ install -p -m 755 bin/copr-rpmbuild-cancel %buildroot%_bindir

+ install -p -m 755 bin/copr-rpmbuild-log %buildroot%_bindir

+ 

+ for script in %buildroot/%{_bindir}/copr-rpmbuild*; do

+     sed -i '1 s|#.*|#! /usr/bin/%python|' "$script"

+ done

  

  name="%{name}" version="%{version}" summary="%{summary}" %py_install

  

  install -p -m 755 copr-update-builder %buildroot%_bindir

  

+ (

+   cd builder-hooks

+   find -name README | while read line; do

+     dir=%buildroot%_sysconfdir"/copr-builder/hooks/$(dirname "$line")"

+     mkdir -p "$dir"

+     install -p -m 644 "$line" "$dir"

+   done

+ )

+ 

  

  %files

  %{!?_licensedir:%global license %doc}
@@ -221,7 +236,7 @@

  

  %{expand:%%%{python}_sitelib}/*

  

- %{_bindir}/copr-rpmbuild

+ %{_bindir}/copr-rpmbuild*

  %{_bindir}/copr-sources-custom

  %{_mandir}/man1/copr-rpmbuild.1*

  
@@ -237,11 +252,18 @@

  %files -n copr-builder

  %license LICENSE

  %_bindir/copr-update-builder

+ %_bindir/copr-builder-cleanup

+ %_sysconfdir/copr-builder

  %dir %mock_config_overrides

  %doc %mock_config_overrides/README

  

  

  %changelog

+ * Tue Jun 09 2020 Pavel Raiskup <praiskup@redhat.com> 0.39-1

+ - more work delegate to builder scripts from backend

+ - don't delete the "old" .rpmnew files

+ - fix macro in comment (rpmlint)

+ 

  * Fri Apr 03 2020 Pavel Raiskup <praiskup@redhat.com> 0.38-1

  - do not scrub mock caches, to re-use dnf/yum caches

  - scrub chroot and bootstrap chroot when build is done

no initial comment

Metadata Update from @praiskup:
- Pull-request tagged with: wip

3 years ago

1 new commit added

  • wip
3 years ago

1 new commit added

  • common: silence pylint warning about classes
3 years ago

1 new commit added

  • fix
3 years ago

1 new commit added

  • test srpm
3 years ago

1 new commit added

  • sign tests
3 years ago

5 new commits added

  • fix tests in mock
  • test
  • verbose tests
  • fix tests
  • fix
3 years ago

1 new commit added

  • fix
3 years ago

1 new commit added

  • fix indefinite loop
3 years ago

3 new commits added

  • fixup
  • style fix
  • drop empty file
3 years ago

5 new commits added

  • test retry
  • another cancel test
  • failed info file
  • test cancel
  • another simplification
3 years ago

3 new commits added

  • a bit more coverage
  • ssh retry
  • test disallowed start
3 years ago

10 new commits added

  • last one
  • 100% coverage
  • one of the last tests
  • another test
  • SEPARATE FIX
  • another test
  • test failed collect
  • fix
  • test createrepo failure
  • drop unused exceptions
3 years ago

1 new commit added

  • mistake fix, document ...
3 years ago

1 new commit added

  • nicer backend.log logging
3 years ago

40 new commits added

  • notice the vm release
  • nicer backend.log logging
  • mistake fix, document ...
  • last one
  • 100% coverage
  • one of the last tests
  • another test
  • SEPARATE FIX
  • another test
  • test failed collect
  • fix
  • test createrepo failure
  • drop unused exceptions
  • a bit more coverage
  • ssh retry
  • test disallowed start
  • test retry
  • another cancel test
  • failed info file
  • test cancel
  • another simplification
  • fixup
  • style fix
  • drop empty file
  • fix indefinite loop
  • fix
  • fix tests in mock
  • test
  • verbose tests
  • fix tests
  • fix
  • sign tests
  • test srpm
  • fix
  • common: silence pylint warning about classes
  • wip
  • wip
  • move to testable place
  • wip
  • backend: CancellableThreadTask cb_cancel can't raise
3 years ago

1 new commit added

  • fixup cancellable task
3 years ago

1 new commit added

  • send the end message
3 years ago

1 new commit added

  • fix, and test
3 years ago

43 new commits added

  • fix, and test
  • send the end message
  • fixup cancellable task
  • notice the vm release
  • nicer backend.log logging
  • mistake fix, document ...
  • last one
  • 100% coverage
  • one of the last tests
  • another test
  • SEPARATE FIX
  • another test
  • test failed collect
  • fix
  • test createrepo failure
  • drop unused exceptions
  • a bit more coverage
  • ssh retry
  • test disallowed start
  • test retry
  • another cancel test
  • failed info file
  • test cancel
  • another simplification
  • fixup
  • style fix
  • drop empty file
  • fix indefinite loop
  • fix
  • fix tests in mock
  • test
  • verbose tests
  • fix tests
  • fix
  • sign tests
  • test srpm
  • fix
  • common: silence pylint warning about classes
  • wip
  • wip
  • move to testable place
  • wip
  • backend: CancellableThreadTask cb_cancel can't raise
3 years ago

rebased onto 4a6186c6a5b4486ecca2a8c7ce213bcd8784bfc4

3 years ago

8 new commits added

  • Automatic commit of package [copr-rpmbuild] release [0.39.dev-1].
  • backend, rpmbuild: delegate more work to builder
  • backend: simplify and cover the build worker code
  • backend: external blob tarball for unittests
  • backend: buggy error handler in pkg_name_evr()
  • backend: don't modify logger record in handler
  • common, backend: disable some PyLint warnings
  • backend: ignore exceptions in CancellableThreadTask
3 years ago

8 new commits added

  • Automatic commit of package [copr-rpmbuild] release [0.39.dev-1].
  • backend, rpmbuild: delegate more work to builder
  • backend: simplify and cover the build worker code
  • backend: external blob tarball for unittests
  • backend: buggy error handler in pkg_name_evr()
  • backend: don't modify logger record in handler
  • common, backend: disable some PyLint warnings
  • backend: ignore exceptions in CancellableThreadTask
3 years ago

I'm removing the WIP flag. There are three changes that I'd like to hack on:
- link the backend.log from frontend build page
- store mock exit status somewhere, and propagate it to frontend
- drop the old VM manager code

But none of those need to be here in this PR.

Metadata Update from @praiskup:
- Pull-request untagged with: wip

3 years ago

1 new commit added

  • frontend: more obvious links to live logs
3 years ago

9 new commits added

  • frontend: more obvious links to live logs
  • Automatic commit of package [copr-rpmbuild] release [0.39.dev-1].
  • backend, rpmbuild: delegate more work to builder
  • backend: simplify and cover the build worker code
  • backend: external blob tarball for unittests
  • backend: buggy error handler in pkg_name_evr()
  • backend: don't modify logger record in handler
  • common, backend: disable some PyLint warnings
  • backend: ignore exceptions in CancellableThreadTask
3 years ago

2 new commits added

  • backend: drop cancel build action code
  • backend: drop the VMM concept
3 years ago

11 new commits added

  • backend: drop cancel build action code
  • backend: drop the VMM concept
  • frontend: more obvious links to live logs
  • Automatic commit of package [copr-rpmbuild] release [0.39.dev-1].
  • backend, rpmbuild: delegate more work to builder
  • backend: simplify and cover the build worker code
  • backend: external blob tarball for unittests
  • backend: buggy error handler in pkg_name_evr()
  • backend: don't modify logger record in handler
  • common, backend: disable some PyLint warnings
  • backend: ignore exceptions in CancellableThreadTask
3 years ago

rebased onto e2c581d6c4d3a1cae8e0bd9652d2ecdf28ede51f

3 years ago

12 new commits added

  • frontend: enable cov by default
  • backend: drop cancel build action code
  • backend: drop the VMM concept
  • frontend: more obvious links to live logs
  • Automatic commit of package [copr-rpmbuild] release [0.39.dev-1].
  • backend, rpmbuild: delegate more work to builder
  • backend: simplify and cover the build worker code
  • backend: external blob tarball for unittests
  • backend: buggy error handler in pkg_name_evr()
  • backend: don't modify logger record in handler
  • common, backend: disable some PyLint warnings
  • backend: ignore exceptions in CancellableThreadTask
3 years ago

1 new commit added

  • backend: fix SRPM/RPM detection from task json
3 years ago

1 new commit added

  • backend: drop another part of VMM
3 years ago

1 new commit added

  • backend: dump attempt to send message to backend.log
3 years ago

rebased onto 0eb6e0a

3 years ago

rebased onto 0eb6e0a

3 years ago

15 new commits added

  • backend: dump attempt to send message to backend.log
  • backend: drop another part of VMM
  • backend: fix SRPM/RPM detection from task json
  • frontend: enable cov by default
  • backend: drop cancel build action code
  • backend: drop the VMM concept
  • frontend: more obvious links to live logs
  • Automatic commit of package [copr-rpmbuild] release [0.39.dev-1].
  • backend, rpmbuild: delegate more work to builder
  • backend: simplify and cover the build worker code
  • backend: external blob tarball for unittests
  • backend: buggy error handler in pkg_name_evr()
  • backend: don't modify logger record in handler
  • common, backend: disable some PyLint warnings
  • backend: ignore exceptions in CancellableThreadTask
3 years ago

I don't know why we have Sourc0 written with macros without curly brackets but the rest of the spec file is and it is a good idea. Can we please have

https://github.com/fedora-copr/%{tests_tar}/archive/v%{tests_version}/%{tests_tar}-%{tests_version}.tar.gz

What's the point of having them here and not in init?

Should we move the exceptions to backend/copr_backend/exceptions.py?

s/it is good idea/it is a good idea to be consistent/ (same as with shell variables, I dislike the curly brackets - but the style has already been given ..., I'll fix)

I didn't want to make them feel like API, is there any benefit? Except for tests I don't want them to appear anywhere else than in this file.

Can we use @pytest.mark.usefixtures here?
Also, I prefer to have test functions inside of a class because then it is easier to group them into smaller chunks if there are some common themes. But it probably doesn't matter here.

The fixture is not only about side effects but is actually used.. I should probably originally start with the class as you'd preffer, or not start using with the fixtures (this is sort of invalid use-case) but the more I wrote, the more lazy I was to restart with writing the test file. :-)

backend: drop another part of VMM

Squash with: a8906a1e5a3d58af093f278cff348b1f3c10f903

Squash If you want to

backend: drop the VMM concept

Do you have any idea how the docker compose reacts to this? I guess we can throw backend-vmm service from docker-compose*.yaml.

But I guess it will break the builder "spawning" there? Probably. There is no reason to block this PR with it, let's fix it in a separate PR and not block a release.

Some small notes but otherwise LGTM.

"it is a good idea to be consistent" true. And I would argue that curly brackets are the only way to do it because if you don't use them and need one macro after another without any separator, you are f-word up.

Except for tests I don't want them to appear anywhere else than in this file.

In this case, we can probably keep them here.

But I guess it will break the builder "spawning" there? Probably. There is no reason to block this PR with it, let's fix it in a separate PR and not block a release.

Yep, I'll try to have a look at fix post-release.

14 new commits added

  • Automatic commit of package [copr-rpmbuild] release [0.39-1].
  • backend: dump attempt to send message to backend.log
  • backend: fix SRPM/RPM detection from task json
  • frontend: enable cov by default
  • backend: drop cancel build action code
  • backend: drop the VMM concept
  • frontend: more obvious links to live logs
  • backend, rpmbuild: delegate more work to builder
  • backend: simplify and cover the build worker code
  • backend: external blob tarball for unittests
  • backend: buggy error handler in pkg_name_evr()
  • backend: don't modify logger record in handler
  • common, backend: disable some PyLint warnings
  • backend: ignore exceptions in CancellableThreadTask
3 years ago

Commit 6f37360 fixes this pull-request

Pull-Request has been merged by praiskup

3 years ago

Thank you for the review.

Metadata
Flags
Copr build
success (100%)
#1433276
3 years ago
Copr build
success (100%)
#1433275
3 years ago
Copr build
success (100%)
#1433274
3 years ago
Copr build
failure
#1433273
3 years ago
jenkins
success (100%)
Build #235 successful (commit: 10507540)
3 years ago
Copr build
success (100%)
#1433065
3 years ago
Copr build
success (100%)
#1433064
3 years ago
Copr build
success (100%)
#1433063
3 years ago
Copr build
failure
#1433062
3 years ago
jenkins
success (100%)
Build #234 successful (commit: 00d7208c)
3 years ago
Copr build
success (100%)
#1433030
3 years ago
Copr build
success (100%)
#1433029
3 years ago
Copr build
success (100%)
#1433028
3 years ago
Copr build
failure
#1433027
3 years ago
Copr build
success (100%)
#1433026
3 years ago
Copr build
success (100%)
#1433025
3 years ago
Copr build
success (100%)
#1433024
3 years ago
Copr build
failure
#1433023
3 years ago
jenkins
success (100%)
Build #233 successful (commit: bfefc762)
3 years ago
Copr build
success (100%)
#1433021
3 years ago
Copr build
success (100%)
#1433020
3 years ago
Copr build
success (100%)
#1433019
3 years ago
Copr build
failure
#1433018
3 years ago
jenkins
failure
Build #232 failed (commit: d00ff4ff)
3 years ago
Copr build
success (100%)
#1432969
3 years ago
Copr build
success (100%)
#1432968
3 years ago
Copr build
success (100%)
#1432967
3 years ago
Copr build
failure
#1432966
3 years ago
jenkins
failure
Build #231 failed (commit: d3a76975)
3 years ago
Copr build
success (100%)
#1431131
3 years ago
Copr build
success (100%)
#1431130
3 years ago
Copr build
success (100%)
#1431129
3 years ago
Copr build
failure
#1431128
3 years ago
jenkins
success (100%)
Build #227 successful (commit: 807d780f)
3 years ago
Copr build
success (100%)
#1431060
3 years ago
Copr build
success (100%)
#1431059
3 years ago
Copr build
success (100%)
#1431058
3 years ago
Copr build
failure
#1431057
3 years ago
jenkins
success (100%)
Build #226 successful (commit: be6f2f21)
3 years ago
Copr build
success (100%)
#1430708
3 years ago
Copr build
success (100%)
#1430707
3 years ago
Copr build
success (100%)
#1430706
3 years ago
Copr build
failure
#1430705
3 years ago
jenkins
success (100%)
Build #225 successful (commit: 6df6cd7c)
3 years ago
Copr build
success (100%)
#1427499
3 years ago
Copr build
success (100%)
#1427498
3 years ago
Copr build
success (100%)
#1427497
3 years ago
Copr build
failure
#1427496
3 years ago
jenkins
success (100%)
Build #221 successful (commit: 3a8de90a)
3 years ago
Copr build
success (100%)
#1427494
3 years ago
Copr build
success (100%)
#1427493
3 years ago
Copr build
success (100%)
#1427492
3 years ago
Copr build
failure
#1427491
3 years ago
jenkins
success (100%)
Build #220 successful (commit: 091c29de)
3 years ago
Copr build
success (100%)
#1427484
3 years ago
Copr build
success (100%)
#1427483
3 years ago
Copr build
success (100%)
#1427482
3 years ago
Copr build
failure
#1427481
3 years ago
jenkins
success (100%)
Build #219 successful (commit: 160dc412)
3 years ago
Copr build
success (100%)
#1427480
3 years ago
Copr build
success (100%)
#1427479
3 years ago
Copr build
success (100%)
#1427478
3 years ago
Copr build
failure
#1427477
3 years ago
jenkins
failure
Build #218 failed (commit: e6da3bf1)
3 years ago
jenkins
success (100%)
Build #216 successful (commit: e6260e09)
3 years ago
Copr build
success (100%)
#1427255
3 years ago
Copr build
success (100%)
#1427254
3 years ago
Copr build
failure
#1427253
3 years ago
Copr build
success (100%)
#1427247
3 years ago
Copr build
success (100%)
#1427246
3 years ago
Copr build
failure
#1427245
3 years ago
Copr build
success (100%)
#1427238
3 years ago
Copr build
success (100%)
#1427237
3 years ago
Copr build
canceled
#1427236
3 years ago
Copr build
success (100%)
#1426759
3 years ago
Copr build
success (100%)
#1426758
3 years ago
Copr build
failure
#1426757
3 years ago
jenkins
success (100%)
Build #215 successful (commit: 2ab3b792)
3 years ago
Copr build
success (100%)
#1426748
3 years ago
Copr build
success (100%)
#1426747
3 years ago
Copr build
failure
#1426746
3 years ago
jenkins
success (100%)
Build #214 successful (commit: 860ec22a)
3 years ago
Copr build
success (100%)
#1426712
3 years ago
Copr build
success (100%)
#1426711
3 years ago
Copr build
failure
#1426710
3 years ago
jenkins
success (100%)
Build #213 successful (commit: 80b44b00)
3 years ago
Copr build
success (100%)
#1424813
3 years ago
Copr build
success (100%)
#1424812
3 years ago
Copr build
failure
#1424811
3 years ago
jenkins
success (100%)
Build #212 successful (commit: a79a5952)
3 years ago
Copr build
success (100%)
#1424809
3 years ago
Copr build
success (100%)
#1424808
3 years ago
Copr build
failure
#1424807
3 years ago
jenkins
success (100%)
Build #211 successful (commit: eeb79045)
3 years ago
Copr build
success (100%)
#1424800
3 years ago
Copr build
success (100%)
#1424799
3 years ago
Copr build
failure
#1424798
3 years ago
jenkins
success (100%)
Build #210 successful (commit: 16962b93)
3 years ago
Copr build
success (100%)
#1424789
3 years ago
Copr build
success (100%)
#1424788
3 years ago
Copr build
failure
#1424787
3 years ago
jenkins
success (100%)
Build #209 successful (commit: 8746a03c)
3 years ago
Copr build
success (100%)
#1424761
3 years ago
Copr build
pending (50%)
#1424760
3 years ago
Copr build
failure
#1424759
3 years ago
jenkins
success (100%)
Build #208 successful (commit: 28d7a96f)
3 years ago
Copr build
success (100%)
#1424246
3 years ago
Copr build
success (100%)
#1424245
3 years ago
Copr build
failure
#1424244
3 years ago
jenkins
success (100%)
Build #207 successful (commit: 81864a08)
3 years ago
Copr build
success (100%)
#1423994
3 years ago
Copr build
success (100%)
#1423993
3 years ago
jenkins
success (100%)
Build #206 successful (commit: 9f0efbef)
3 years ago
Copr build
failure
#1423992
3 years ago
Copr build
success (100%)
#1422545
3 years ago
Copr build
success (100%)
#1422544
3 years ago
Copr build
failure
#1422543
3 years ago
jenkins
success (100%)
Build #205 successful (commit: 417f8de9)
3 years ago
Copr build
success (100%)
#1421217
3 years ago
Copr build
success (100%)
#1421216
3 years ago
Copr build
failure
#1421215
3 years ago
jenkins
success (100%)
Build #203 successful (commit: 9bd19df8)
3 years ago
Copr build
success (100%)
#1421177
3 years ago
Copr build
success (100%)
#1421176
3 years ago
jenkins
success (100%)
Build #202 successful (commit: 3a95bd02)
3 years ago
Copr build
failure
#1421175
3 years ago
Copr build
success (100%)
#1419870
3 years ago
Copr build
success (100%)
#1419869
3 years ago
jenkins
success (100%)
Build #201 successful (commit: 4da15e03)
3 years ago
Copr build
failure
#1419868
3 years ago
Copr build
success (100%)
#1419783
3 years ago
Copr build
success (100%)
#1419782
3 years ago
Copr build
failure
#1419781
3 years ago
jenkins
success (100%)
Build #200 successful (commit: edd79d03)
3 years ago
Copr build
success (100%)
#1419730
3 years ago
Copr build
success (100%)
#1419729
3 years ago
Copr build
failure
#1419728
3 years ago
jenkins
failure
Build #199 failed (commit: 49988b98)
3 years ago
jenkins
success (100%)
Build #198 successful (commit: 66119382)
3 years ago
Copr build
success (100%)
#1419550
3 years ago
Copr build
success (100%)
#1419549
3 years ago
Copr build
failure
#1419548
3 years ago
Copr build
success (100%)
#1419512
3 years ago
Copr build
success (100%)
#1419511
3 years ago
Copr build
failure
#1419510
3 years ago
jenkins
success (100%)
Build #197 successful (commit: a8a56b9c)
3 years ago
jenkins
failure
Build #196 failed (commit: e4d0efbb)
3 years ago
Copr build
success (100%)
#1419506
3 years ago
Copr build
success (100%)
#1419505
3 years ago
Copr build
failure
#1419504
3 years ago
Copr build
pending (50%)
#1419405
3 years ago
Copr build
success (100%)
#1419404
3 years ago
jenkins
failure
Build #195 failed (commit: 64e13c79)
3 years ago
Copr build
failure
#1419403
3 years ago
Changes Summary 65
+4 -0
file changed
.gitignore
+36
file added
.tito/library/builder.py
+1 -1
file changed
.tito/packages/copr-rpmbuild
+2 -1
file changed
.tito/tito.props
+0 -32
file changed
backend/conf/copr-be.conf.example
+9 -2
file changed
backend/copr-backend.spec
+1 -54
file changed
backend/copr_backend/actions.py
+7 -2
file changed
backend/copr_backend/background_worker.py
+751
file added
backend/copr_backend/background_worker_build.py
+11 -4
file changed
backend/copr_backend/cancellable_thread.py
-228
file removed
backend/copr_backend/daemons/vm_master.py
-379
file removed
backend/copr_backend/daemons/worker.py
+2 -15
file changed
backend/copr_backend/exceptions.py
+7 -1
file changed
backend/copr_backend/helpers.py
+22 -5
file changed
backend/copr_backend/job.py
-330
file removed
backend/copr_backend/mockremote/__init__.py
-185
file removed
backend/copr_backend/mockremote/builder.py
+48 -9
file changed
backend/copr_backend/msgbus.py
+10 -7
file changed
backend/copr_backend/rpm_builds.py
+101 -6
file changed
backend/copr_backend/sshcmd.py
-39
file removed
backend/copr_backend/vm_manage/__init__.py
-65
file removed
backend/copr_backend/vm_manage/check.py
-176
file removed
backend/copr_backend/vm_manage/event_handle.py
-74
file removed
backend/copr_backend/vm_manage/executor.py
-458
file removed
backend/copr_backend/vm_manage/manager.py
-76
file removed
backend/copr_backend/vm_manage/models.py
-142
file removed
backend/copr_backend/vm_manage/spawn.py
-76
file removed
backend/copr_backend/vm_manage/terminate.py
+0 -6
file changed
backend/docker/files/etc/supervisord.conf
-159
file removed
backend/run/cleanup_vm_nova.py
+1 -168
file changed
backend/run/copr-backend-process-build
+9 -6
file changed
backend/run/copr-repo
-16
file removed
backend/run/copr_get_vm_info.py
-56
file removed
backend/run/copr_run_vmm.py
+17 -1
file changed
backend/run_tests.sh
+3 -0
file changed
backend/tests/conftest.py
-520
file removed
backend/tests/daemons/test_dispatcher.py
-604
file removed
backend/tests/daemons/test_vm_master.py
+5
file added
backend/tests/fake-bin-sign
-802
file removed
backend/tests/mockremote/test_builder.py
-285
file removed
backend/tests/mockremote/test_mockremote.py
+860
file added
backend/tests/test_background_worker_build.py
+4 -3
file changed
backend/tests/test_helpers.py
+188 -0
file changed
backend/tests/testlib/__init__.py
-176
file removed
backend/tests/vm_manager/test_check.py
-361
file removed
backend/tests/vm_manager/test_event_handle.py
-99
file removed
backend/tests/vm_manager/test_executor.py
-442
file removed
backend/tests/vm_manager/test_manager.py
-227
file removed
backend/tests/vm_manager/test_spawn.py
-187
file removed
backend/tests/vm_manager/test_terminate.py
+2 -2
file changed
backend/units/copr-backend-log.service
-15
file removed
backend/units/copr-backend-vmm.service
+1 -1
file changed
backend/units/copr-backend.target
+2 -0
file changed
common/copr_common/enums.py
+1 -0
file changed
frontend/copr-frontend.spec
+83 -15
file changed
frontend/coprs_frontend/coprs/models.py
+14 -7
file changed
frontend/coprs_frontend/coprs/templates/coprs/detail/build.html
+1 -1
file changed
frontend/coprs_frontend/coprs/views/apiv3_ns/apiv3_builds.py
+95 -0
file changed
frontend/coprs_frontend/tests/test_models.py
+3 -1
file changed
frontend/run_tests.sh
+42
file added
rpmbuild/bin/copr-builder-cleanup
+81
file added
rpmbuild/bin/copr-rpmbuild-cancel
+73
file added
rpmbuild/bin/copr-rpmbuild-log
+2
file added
rpmbuild/builder-hooks/cleanup/README
+25 -3
file changed
rpmbuild/copr-rpmbuild.spec