blob: 296349804ae85c595e99a2754a9006bd928c1448 [file] [log] [blame] [edit]
#!/usr/bin/python3
# Copyright (C) 2019 The Android Open Source Project
#
# 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.
import hashlib
import os
import shutil
import time
from abc import ABC, abstractmethod
from zipfile import ZipFile
import requests
from .reindex import IndexType, get_index_type
from ..constants import SITE_PATH, SITE_PLUGIN_PATH, SITE_LIB_PATH
from ..helpers import log
from ..config.cluster_mode import ClusterMode
LOG = log.get_logger("init")
MAX_LOCK_LIFETIME = 60
MAX_CACHED_VERSIONS = 5
REQUIRED_PLUGINS = ["healthcheck"]
REQUIRED_HA_PLUGINS = ["high-availability"]
REQUIRED_HA_LIBS = ["high-availability", "global-refdb"]
REQUIRED_MULTISITE_LIBS = [
"events-broker",
"global-refdb",
"multi-site",
"pull-replication",
]
REQUIRED_MULTISITE_PLUGINS = [
"events-kafka",
"multi-site",
"pull-replication",
"websession-broker",
]
class InvalidPluginException(Exception):
"""Exception to be raised, if the downloaded plugin is not valid."""
class MissingRequiredPluginException(Exception):
"""Exception to be raised, if the downloaded plugin is not valid."""
class AbstractPluginInstaller(ABC):
def __init__(self, gerrit_config, config):
self.gerrit_config = gerrit_config
self.config = config
self.required_plugins = self._get_required_plugins()
self.required_libs = self._get_required_libs()
self.plugins_changed = False
def _create_plugins_dir(self):
if not os.path.exists(SITE_PLUGIN_PATH):
os.makedirs(SITE_PLUGIN_PATH)
LOG.info("Created plugin installation directory: %s", SITE_PLUGIN_PATH)
def _create_lib_dir(self):
if not os.path.exists(SITE_LIB_PATH):
os.makedirs(SITE_LIB_PATH)
LOG.info("Created lib installation directory: %s", SITE_LIB_PATH)
def _get_installed_plugins(self):
return self._get_installed_jars(SITE_PLUGIN_PATH)
def _get_installed_libs(self):
return self._get_installed_jars(SITE_LIB_PATH)
@staticmethod
def _get_installed_jars(dir):
if os.path.exists(dir):
return [f for f in os.listdir(dir) if f.endswith(".jar")]
return []
def _get_required_plugins(self):
required = REQUIRED_PLUGINS.copy()
if self.config.is_ha:
required.extend(REQUIRED_HA_PLUGINS)
elif self.config.cluster_mode == ClusterMode.MULTISITE:
required.extend(REQUIRED_MULTISITE_PLUGINS)
if self.config.refdb:
required.append(f"{self.config.refdb}-refdb")
if get_index_type(self.gerrit_config) == IndexType.ELASTICSEARCH:
required.append("index-elasticsearch")
LOG.info(
"Requiring plugins (ClusterMode: %s): %s",
self.config.cluster_mode.name,
required,
)
return required
def _get_required_libs(self):
required = []
if self.config.is_ha:
required.extend(REQUIRED_HA_LIBS)
elif self.config.cluster_mode == ClusterMode.MULTISITE:
required.extend(REQUIRED_MULTISITE_LIBS)
elif self.config.refdb:
required.append("global-refdb")
if get_index_type(self.gerrit_config) == IndexType.ELASTICSEARCH:
required.append("index-elasticsearch")
LOG.info(
"Requiring libs (ClusterMode: %s): %s",
self.config.cluster_mode.name,
required,
)
return required
def _install_required_plugins(self):
for plugin in self.required_plugins:
if plugin in self.config.get_plugin_names():
continue
self._install_required_jar(plugin, SITE_PLUGIN_PATH)
def _install_required_libs(self):
for lib in self.required_libs:
if lib in self.config.get_lib_names():
continue
self._install_required_jar(lib, SITE_LIB_PATH)
def _install_required_jar(self, jar, target_dir):
with ZipFile("/var/war/gerrit.war", "r") as war:
# Lib modules can be packaged as a plugin. However, they could
# currently not be installed by the init pgm tool.
if f"WEB-INF/plugins/{jar}.jar" in war.namelist():
self._install_plugin_from_war(jar, target_dir)
return
try:
self._install_jar_from_container(jar, target_dir)
except FileNotFoundError:
raise MissingRequiredPluginException(f"Required jar {jar} was not found.")
def _install_jar_from_container(self, plugin, target_dir):
source_file = os.path.join("/var/plugins", plugin + ".jar")
target_file = os.path.join(target_dir, plugin + ".jar")
LOG.info(
"Installing plugin %s from container to %s.",
plugin,
target_file,
)
if not os.path.exists(source_file):
raise FileNotFoundError(
"Unable to find required plugin in container: " + plugin
)
if os.path.exists(target_file) and self._get_file_sha(
source_file
) == self._get_file_sha(target_file):
return
shutil.copyfile(source_file, target_file)
self.plugins_changed = True
def _install_plugins_from_war(self):
for plugin in self.config.get_packaged_plugins():
self._install_plugin_from_war(plugin["name"], SITE_PLUGIN_PATH)
def _install_plugin_from_war(self, plugin, target_dir):
LOG.info("Installing packaged plugin %s.", plugin)
with ZipFile("/var/war/gerrit.war", "r") as war:
war.extract(f"WEB-INF/plugins/{plugin}.jar", SITE_PLUGIN_PATH)
source_file = f"{SITE_PLUGIN_PATH}/WEB-INF/plugins/{plugin}.jar"
target_file = os.path.join(target_dir, f"{plugin}.jar")
if not os.path.exists(target_file) or self._get_file_sha(
source_file
) != self._get_file_sha(target_file):
os.rename(source_file, target_file)
self.plugins_changed = True
shutil.rmtree(os.path.join(SITE_PLUGIN_PATH, "WEB-INF"), ignore_errors=True)
@staticmethod
def _get_file_sha(file):
file_hash = hashlib.sha1()
with open(file, "rb") as f:
while True:
chunk = f.read(64000)
if not chunk:
break
file_hash.update(chunk)
LOG.debug("SHA1 of file '%s' is %s", file, file_hash.hexdigest())
return file_hash.hexdigest()
def _remove_unwanted_plugins(self):
wanted_plugins = list(self.config.get_plugins())
wanted_plugins.extend(self.required_plugins)
self._remove_unwanted(
wanted_plugins, self._get_installed_plugins(), SITE_PLUGIN_PATH
)
def _remove_unwanted_libs(self):
wanted_libs = list(self.config.get_libs())
wanted_libs.extend(self.required_libs)
wanted_libs.extend(self.config.get_plugins_installed_as_lib())
self._remove_unwanted(wanted_libs, self._get_installed_libs(), SITE_LIB_PATH)
@staticmethod
def _remove_unwanted(wanted, installed, dir):
for plugin in installed:
if os.path.splitext(plugin)[0] not in wanted:
os.remove(os.path.join(dir, plugin))
LOG.info("Removed plugin %s", plugin)
def _symlink_plugins_to_lib(self):
if not os.path.exists(SITE_LIB_PATH):
os.makedirs(SITE_LIB_PATH)
else:
for f in os.listdir(SITE_LIB_PATH):
path = os.path.join(SITE_LIB_PATH, f)
if (
os.path.islink(path)
and os.path.splitext(f)[0]
not in self.config.get_plugins_installed_as_lib()
):
os.unlink(path)
LOG.info("Removed symlink %s", f)
for lib in self.config.get_plugins_installed_as_lib():
plugin_path = os.path.join(SITE_PLUGIN_PATH, f"{lib}.jar")
if os.path.exists(plugin_path):
try:
os.symlink(plugin_path, os.path.join(SITE_LIB_PATH, f"{lib}.jar"))
except FileExistsError:
continue
else:
raise FileNotFoundError(
f"Could not find plugin {lib} to symlink to lib-directory."
)
def execute(self):
self._create_plugins_dir()
self._create_lib_dir()
self._remove_unwanted_plugins()
self._remove_unwanted_libs()
self._install_required_plugins()
self._install_required_libs()
self._install_plugins_from_war()
for plugin in self.config.get_downloaded_plugins():
self._install_plugin(plugin)
for plugin in self.config.get_libs():
self._install_lib(plugin)
self._symlink_plugins_to_lib()
def _download_plugin(self, plugin, target):
LOG.info("Downloading %s plugin to %s", plugin["name"], target)
try:
response = requests.get(plugin["url"])
except requests.exceptions.SSLError:
response = requests.get(plugin["url"], verify=self.config.ca_cert_path)
with open(target, "wb") as f:
f.write(response.content)
file_sha = self._get_file_sha(target)
if file_sha != plugin["sha1"]:
os.remove(target)
raise InvalidPluginException(
(
f"SHA1 of downloaded file ({file_sha}) did not match "
f"expected SHA1 ({plugin['sha1']}). "
f"Removed downloaded file ({target})"
)
)
def _install_plugin(self, plugin):
self._install_jar(plugin, SITE_PLUGIN_PATH)
def _install_lib(self, lib):
self._install_jar(lib, SITE_LIB_PATH)
@abstractmethod
def _install_jar(self, plugin, target_dir):
pass
class PluginInstaller(AbstractPluginInstaller):
def _install_jar(self, plugin, target_dir):
target = os.path.join(target_dir, f"{plugin['name']}.jar")
if os.path.exists(target) and self._get_file_sha(target) == plugin["sha1"]:
return
self._download_plugin(plugin, target)
self.plugins_changed = True
class CachedPluginInstaller(AbstractPluginInstaller):
@staticmethod
def _cleanup_cache(plugin_cache_dir):
cached_files = [
os.path.join(plugin_cache_dir, f) for f in os.listdir(plugin_cache_dir)
]
while len(cached_files) > MAX_CACHED_VERSIONS:
oldest_file = min(cached_files, key=os.path.getctime)
LOG.info(
"Too many cached files in %s. Removing file %s",
plugin_cache_dir,
oldest_file,
)
os.remove(oldest_file)
cached_files.remove(oldest_file)
@staticmethod
def _create_download_lock(lock_path):
with open(lock_path, "w", encoding="utf-8") as f:
f.write(os.environ["HOSTNAME"])
LOG.debug("Created download lock %s", lock_path)
@staticmethod
def _create_plugin_cache_dir(plugin_cache_dir):
if not os.path.exists(plugin_cache_dir):
os.makedirs(plugin_cache_dir)
LOG.info("Created cache directory %s", plugin_cache_dir)
def _get_cached_plugin_path(self, plugin):
return os.path.join(
self.config.plugin_cache_dir,
plugin["name"],
f"{plugin['name']}-{plugin['sha1']}.jar",
)
def _install_from_cache_or_download(self, plugin, target):
cached_plugin_path = self._get_cached_plugin_path(plugin)
if os.path.exists(cached_plugin_path):
LOG.info("Installing %s plugin from cache.", plugin["name"])
else:
LOG.info("%s not found in cache. Downloading it.", plugin["name"])
self._create_plugin_cache_dir(os.path.dirname(cached_plugin_path))
lock_path = f"{cached_plugin_path}.lock"
while os.path.exists(lock_path):
LOG.info(
"Download lock found (%s). Waiting %d seconds for it to be released.",
lock_path,
MAX_LOCK_LIFETIME,
)
lock_timestamp = os.path.getmtime(lock_path)
if time.time() > lock_timestamp + MAX_LOCK_LIFETIME:
LOG.info("Stale download lock found (%s).", lock_path)
self._remove_download_lock(lock_path)
self._create_download_lock(lock_path)
try:
self._download_plugin(plugin, cached_plugin_path)
finally:
self._remove_download_lock(lock_path)
shutil.copy(cached_plugin_path, target)
self._cleanup_cache(os.path.dirname(cached_plugin_path))
def _install_jar(self, plugin, target_dir):
install_path = os.path.join(target_dir, f"{plugin['name']}.jar")
if (
os.path.exists(install_path)
and self._get_file_sha(install_path) == plugin["sha1"]
):
return
self.plugins_changed = True
self._install_from_cache_or_download(plugin, install_path)
@staticmethod
def _remove_download_lock(lock_path):
os.remove(lock_path)
LOG.debug("Removed download lock %s", lock_path)
def get_installer(gerrit_config, config):
plugin_installer = (
CachedPluginInstaller if config.plugin_cache_enabled else PluginInstaller
)
return plugin_installer(gerrit_config, config)