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

311 lines
12 KiB
Python
Raw Normal View History

2016-06-17 01:36:34 +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-17 01:26:56 +02:00
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-20 02:29:46 +02:00
from subprocess import Popen, PIPE, check_call
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, Thread
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)
self._output_dir = 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 clean_working_dir(self, abort_running=True):
Thread(target=self.clean_working_dir_blocking).start()
def clean_working_dir_blocking(self, abort_running=True):
def clean_fn():
2016-06-20 02:29:46 +02:00
rmpaths = map(shlex.split, [str(self.build_repo_path),
str(self._output_dir)])
for p in rmpaths:
check_call(["rm", "-rf"] + p)
with self._build_lock:
if abort_running:
self.try_abort_build()
# cancel everything, so we are next
for fut in self._futures.copy():
fut.cancel()
if fut.done():
self._futures.remove(fut)
def build_job():
log.info("Starting cleaning of working dir!")
self.update_status(True, "Starting cleaning of working dir!",
running=False)
try:
exception_logged(clean_fn, log.error)()
except Exception as e:
self.update_status(False, "Cleaning failed!",
running=False, payload={"exception": e})
raise
future = self._executor.submit(build_job)
self._futures.add(future)
future.result()
log.info("Working dir cleand!")
self.update_status(True, "Working dir cleand!", running=False)
2016-06-14 21:16:23 +02:00
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)
results = repo.submodule_sync_update_init_recursive_force()
for r in results:
log_git(r)
def build(self, abort_running=False, wait=False, ignore_pull_error=False,
build_fn=None):
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
build_bl = partial(self._build_blocking, ignore_pull_error=
2016-06-14 21:16:23 +02:00
ignore_pull_error)
build_fn = build_fn if build_fn else build_bl
def build_job():
build_func = exception_logged(build_fn, log.error)
2016-06-14 21:16:23 +02:00
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)