1
0
mirror of https://github.com/IEEE-SB-Passau/pelican-deployment-system.git synced 2017-09-06 16:35:38 +02:00
Files
pelican-deployment-system/pelican_deploy/deploy.py

270 lines
10 KiB
Python
Raw Normal View History

2016-06-17 01:26:56 +02:00
Copyright 2016 Peter Dahlberg
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
2016-06-10 21:59:52 +02:00
from pathlib import Path
2016-06-14 21:16:23 +02:00
from collections import namedtuple, deque
2016-06-12 22:46:27 +02:00
from pelican_deploy.gittool import Repo, log_git_result
from functools import partial
2016-06-11 23:30:27 +02:00
from subprocess import Popen, PIPE
2016-06-13 00:58:13 +02:00
from pelican_deploy.util import exception_logged
2016-06-10 21:59:52 +02:00
from concurrent.futures import ThreadPoolExecutor
from threading import RLock
2016-06-14 21:16:23 +02:00
from datetime import datetime
import pytz
2016-06-10 21:59:52 +02:00
import sys
import logging
import shlex
import os
log = logging.getLogger(__name__)
2016-06-12 22:46:27 +02:00
log_git = partial(log_git_result, out_logger=log.debug,
err_logger=log.debug, status_logger=log.debug)
2016-06-16 23:21:05 +02:00
TOX_RESULT_FILE = "{name}_result.json"
2016-06-11 13:19:54 +02:00
BUILD_REPO_DIR = "{name}_build_repo"
OUTPUT_DIR = "{name}_output"
2016-06-14 21:16:23 +02:00
STATUS_LEN = 500
BuildStatus = namedtuple("BuildStatus", "date ok msg payload running")
2016-06-10 21:59:52 +02:00
class PullError(Exception):
pass
2016-06-10 21:59:52 +02:00
class DeploymentRunner:
2016-06-10 21:59:52 +02:00
def __init__(self, name, runner_config):
self.name = name
self.working_directory = Path(runner_config["working_directory"])
if not self.working_directory.exists():
log.info("creating working directory for %s: %s", name,
self.working_directory)
self.working_directory.mkdir(parents=True)
self.working_directory = self.working_directory.resolve()
self.clone_url = runner_config["clone_url"]
self.git_branch = runner_config["git_branch"]
2016-06-11 13:19:54 +02:00
self.build_repo_path = self.working_directory / BUILD_REPO_DIR.format(
name=name)
2016-06-11 22:30:22 +02:00
outdir = self.working_directory / OUTPUT_DIR.format(name=name)
2016-06-16 23:21:05 +02:00
toxresult = self.working_directory / TOX_RESULT_FILE.format(name=name)
2016-06-11 22:04:27 +02:00
self.build_command = runner_config["build_command"].format(
2016-06-16 23:21:05 +02:00
output=outdir, toxresult=toxresult)
self.final_install_command = runner_config["final_install_command"]\
.format(output=outdir)
2016-06-11 15:38:59 +02:00
self._build_proc_env = dict(os.environ,
2016-06-11 22:04:27 +02:00
**runner_config.get("build_env", {}))
2016-06-10 21:59:52 +02:00
2016-06-11 15:38:59 +02:00
self._executor = ThreadPoolExecutor(max_workers=1)
self._futures = set()
self._build_proc = None
self._abort = False
2016-06-10 21:59:52 +02:00
self._build_lock = RLock()
2016-06-13 00:58:13 +02:00
self._repo_update_lock = RLock()
2016-06-10 21:59:52 +02:00
2016-06-14 21:16:23 +02:00
self.build_status = deque(maxlen=STATUS_LEN)
def update_status(self, ok, msg, payload=None, running=True):
date = pytz.utc.localize(datetime.utcnow())
self.build_status.append(BuildStatus(date, ok, msg, payload, running))
2016-06-10 21:59:52 +02:00
def update_build_repository(self):
2016-06-13 00:58:13 +02:00
with self._repo_update_lock:
self._update_build_repository()
def _update_build_repository(self):
2016-06-13 01:51:39 +02:00
if not self.build_repo_path.exists():
self.build_repo_path.mkdir(parents=True)
2016-06-12 22:26:32 +02:00
repo = Repo(str(self.build_repo_path))
if not repo.is_repo():
if self.build_repo_path.is_dir() and \
2016-06-10 21:59:52 +02:00
next(self.build_repo_path.iterdir(), None) is not None:
2016-06-11 13:19:54 +02:00
log.error(
"non-empty %s exists but not a valid git repository!",
self.build_repo_path)
2016-06-12 22:26:32 +02:00
raise RuntimeException(("non-empty {} exists but not a"
"valid git repository!").format(self.build_repo_path))
2016-06-10 21:59:52 +02:00
else:
2016-06-13 01:51:39 +02:00
log.info("Build repository %s not there, cloning",
self.build_repo_path)
result = repo.clone("--branch", self.git_branch,
2016-06-12 22:26:32 +02:00
"--depth", "1", self.clone_url, ".")
2016-06-12 22:46:27 +02:00
log_git(result)
2016-06-10 21:59:52 +02:00
2016-06-12 22:26:32 +02:00
origin_url = repo.config_get("remote.origin.url")
if origin_url != self.clone_url:
2016-06-11 23:30:27 +02:00
log.info("%s build_repo: URL of git origin changed (`%s` --> `%s`),\
2016-06-12 22:26:32 +02:00
adjusting...", self.name, origin_url, self.clone_url)
repo.config("remote.origin.url", self.clone_url)
2016-06-10 21:59:52 +02:00
# deinit submodules to avoid removed ones dangling around later
# they should stay around in .git, so reinit should be fast
2016-06-13 16:05:22 +02:00
result = repo.submodule("deinit", "--force", ".")
2016-06-12 22:46:27 +02:00
log_git(result)
2016-06-13 01:25:02 +02:00
result = repo.checkout("--force", self.git_branch)
log_git(result)
2016-06-12 22:46:27 +02:00
result = repo.reset("--hard")
log_git(result)
2016-06-10 21:59:52 +02:00
2016-06-12 22:26:32 +02:00
log.info("%s build_repo: pulling changes from origin", self.name)
refspec = "+{b}:{b}".format(b=self.git_branch)
try:
2016-06-14 15:58:05 +02:00
result = repo.pull("--force", "--recurse-submodules",
"--depth", "1", "origin", refspec)
log_git(result)
except Exception as e:
# need to reinit the submodules
self._update_build_repo_submodules(repo)
raise PullError from e
2016-06-12 22:46:27 +02:00
2016-06-10 21:59:52 +02:00
try:
2016-06-12 22:46:27 +02:00
result = repo.clean("--force", "-d", "-x")
log_git(result)
2016-06-10 21:59:52 +02:00
except:
log.warning("git clean failed!", exc_info=True)
# update the submodules
self._update_build_repo_submodules(repo)
def _update_build_repo_submodules(self, repo):
log.info("%s build_repo: update submodules", self.name)
2016-06-12 22:46:27 +02:00
result = repo.submodule("update", "--init", "--force", "--recursive")
log_git(result)
def build(self, abort_running=False, wait=False, ignore_pull_error=False):
2016-06-10 21:59:52 +02:00
with self._build_lock:
if abort_running:
self.try_abort_build()
# cancel everything, so we are next
2016-06-11 15:38:59 +02:00
for fut in self._futures.copy():
2016-06-10 21:59:52 +02:00
fut.cancel()
if fut.done():
2016-06-11 15:38:59 +02:00
self._futures.remove(fut)
2016-06-10 21:59:52 +02:00
2016-06-14 21:16:23 +02:00
def build_job():
build_bl = partial(self._build_blocking, ignore_pull_error=
ignore_pull_error)
build_func = exception_logged(build_bl, log.error)
try:
build_func()
except Exception as e:
self.update_status(False, "Build stopped with exception",
running=False, payload={"exception": e})
raise
future = self._executor.submit(build_job)
2016-06-13 16:05:22 +02:00
self._futures.add(future)
if wait:
return future.result()
2016-06-10 21:59:52 +02:00
def try_abort_build(self):
2016-06-11 15:38:59 +02:00
proc = self._build_proc
self._abort = True
2016-06-10 21:59:52 +02:00
if proc:
2016-06-14 16:35:50 +02:00
try:
proc.kill()
except:
log.debug("unable to kill", exc_info=True)
2016-06-10 21:59:52 +02:00
def final_install(self):
args = shlex.split(self.final_install_command)
2016-06-15 00:15:11 +02:00
self.update_status(True, "Starting final_install",
payload={"cmd": args})
log.info("%s: Starting final_install `%s`", self.name, args)
2016-06-13 23:10:52 +02:00
proc = Popen(args, stdout=PIPE, stderr=PIPE, universal_newlines=True,
start_new_session=True)
outs, errs = proc.communicate()
status = proc.wait()
if status < 0:
log.info("%s: killed final_install_command (%s)", self.name, status)
else:
log.info("%s: finished final_install_command with status %s!",
self.name, status)
2016-06-12 22:54:46 +02:00
log.info('%s final_install_command stdout: %s\n', self.name, outs)
log.info('%s final_install_command stderr: %s\n', self.name, errs)
if status > 0:
2016-06-14 21:16:23 +02:00
self.update_status(False, ("final_install_command failed."
" Website may be broken!"),
payload={"status": status,
"stdout": outs, "stderr": errs})
log.error("%s: final_install failed! Website may be broken!",
self.name)
2016-06-14 21:16:23 +02:00
else:
self.update_status(True, "finished final_install_command",
payload={"stdout": outs, "stderr": errs})
2016-06-14 21:16:23 +02:00
def _build_blocking(self, ignore_pull_error=False):
2016-06-11 15:38:59 +02:00
self._abort = False
2016-06-10 21:59:52 +02:00
# preparing build environment
try:
2016-06-14 21:16:23 +02:00
self.update_status(True, "Start updating repository")
self.update_build_repository()
except PullError:
if ignore_pull_error:
2016-06-14 21:16:23 +02:00
msg = "Git pull failed, trying to continue with what we have"
self.update_status(False, msg)
log.warning(msg, exc_info=True)
else:
raise
2016-06-10 21:59:52 +02:00
# start the build if we should not abort
2016-06-11 15:38:59 +02:00
if not self._abort:
2016-06-11 22:04:27 +02:00
args = shlex.split(self.build_command)
2016-06-14 21:16:23 +02:00
self.update_status(True, "Starting the main build command",
payload={"cmd": args})
2016-06-11 23:30:27 +02:00
log.info("%s: Starting build_command `%s`", self.name, args)
self._build_proc = Popen(args, stdout=PIPE, stderr=PIPE,
2016-06-11 15:38:59 +02:00
cwd=str(self.build_repo_path),
2016-06-12 22:54:46 +02:00
env=self._build_proc_env,
2016-06-13 23:10:52 +02:00
universal_newlines=True,
start_new_session=True)
2016-06-11 23:30:27 +02:00
outs, errs = self._build_proc.communicate()
2016-06-11 15:38:59 +02:00
status = self._build_proc.wait()
2016-06-16 23:21:23 +02:00
self._build_proc = None
2016-06-11 01:07:18 +02:00
2016-06-11 23:30:27 +02:00
if status < 0:
2016-06-14 21:16:23 +02:00
self.update_status(False, "killed build_command")
2016-06-11 23:30:27 +02:00
log.info("%s: killed build_command", self.name)
else:
log.info("%s: finished build_command with status %s!",
self.name, status)
2016-06-12 22:54:46 +02:00
log.info('%s build_command stdout: %s\n', self.name, outs)
log.info('%s build_command stderr: %s\n', self.name, errs)
2016-06-10 21:59:52 +02:00
if status == 0:
2016-06-14 21:16:23 +02:00
self.update_status(True, "finished build_command",
payload={"stdout": outs, "stderr": errs})
self.final_install()
2016-06-14 21:16:23 +02:00
else:
self.update_status(False, "build_command failed",
payload={"status": status,
"stdout": outs, "stderr": errs})
self.update_status(self.build_status[-1].ok, "End of build",
running=False)
2016-06-13 23:10:52 +02:00
def shutdown(self):
self.try_abort_build()
self._executor.shutdown(wait=True)