# -*- coding: utf-8 -*-
"""
scap.config
~~~~~~~~~~~
Configuration management
Copyright © 2014-2017 Wikimedia Foundation and Contributors.
This file is part of Scap.
Scap 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, version 3.
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, see <http://www.gnu.org/licenses/>.
"""
import getpass
import os
import socket
from configparser import ConfigParser
import scap.utils as utils
DEFAULT_CONFIG = {
"beta_only_config_files": (str, ""),
"block_deployments": (bool, False),
"canary_dashboard_url": (str, "https://logstash.wikimedia.org"),
"canary_threshold": (int, 10),
"canary_service": (str, "mediawiki"),
"canary_wait_time": (int, 20),
"testservers_check_cmd_baremetal": (str, ""),
"testservers_check_cmd_k8s": (str, ""),
"delay_messageblobstore_purge": (bool, False),
"deploy_dir": (str, "/srv/mediawiki"),
"failure_limit": (str, "0%"),
"gerrit_url": (str, "https://gerrit.wikimedia.org/r/"),
"gerrit_push_url": (str, "ssh://gerrit.wikimedia.org:29418/"),
"gerrit_push_user": (str, None),
"php_version": (str, "php"),
"keyholder_key": (str, None),
"stage_dir": (str, "/srv/mediawiki-staging"),
"lock_file": (str, None),
"log_json": (bool, False),
"logstash_host": (str, "logstash1001.eqiad.wmnet:9200"),
"mw_web_clusters": (str, "jobrunner,appserver,appserver_api,testserver"),
"manage_mediawiki_php_symlink": (bool, True),
"master_rsync": (str, "localhost"),
"statsd_host": (str, "127.0.0.1"),
"statsd_port": (str, "2003"),
"tcpircbot_host": (str, None),
"tcpircbot_port": (str, "9200"),
"udp2log_host": (str, None),
"udp2log_port": (str, "8420"),
"wmf_realm": (str, "production"),
"ssh_user": (str, getpass.getuser()),
"datacenter": (str, "eqiad"),
"dsh_testservers": (str, "testserver"),
"dsh_targets": (str, "mediawiki-installation"),
"dsh_masters": (str, "scap-masters"),
"dsh_proxies": (str, "scap-proxies"),
"group_size": (int, None),
"git_deploy_dir": (str, "/srv/deployment"),
"git_fat": (bool, False),
"git_binary_manager": (str, None),
"git_server": (str, None),
"git_scheme": (str, "http"),
"git_submodules": (bool, False),
"git_upstream_submodules": (bool, False),
"config_deploy": (bool, False),
"operations_mediawiki_config_branch": (str, "master"),
"nrpe_dir": (str, "/etc/nagios/nrpe.d"),
"require_valid_service": (bool, False),
"scap3_mediawiki": (bool, False),
"secondary_host_signal_file": (str, "/etc/scap.secondary"),
"service_timeout": (float, 120.0),
"tags_to_keep": (int, 20),
"perform_checks": (bool, True),
"patch_path": (str, "/srv/patches"),
"php7_admin_port": (int, None),
"php_fpm_opcache_threshold": (int, 100),
"php_fpm_always_restart": (bool, False),
"php_l10n": (bool, False), # Feature flag for PHP l10n file generation
"cache_revs": (int, 5),
"use_syslog": (bool, False),
"umask": (int, 0o002),
"require_terminal_multiplexer": (bool, False),
"train_blockers_url": (str, "https://train-blockers.toolforge.org/api.php"),
# HTTP/HTTPS proxy used only for requests that need it (such as accessing train_blockers_url)
"web_proxy": (str, None),
# How long scap will wait for a gerrit patch with a version change to complete CI. Default is 5m
"version_update_patch_timeout": (int, 300),
# Settings related to building and deploying mediawiki container image
# xref AbstractSync._build_container_images() in main.py
"build_mw_container_image": (bool, False),
"full_image_build": (bool, False),
"deploy_mw_container_image": (bool, False),
"release_repo_dir": (str, None),
"release_repo_update_cmd": (str, None),
# This command will run with the current directory set to
# {release_repo_dir}/make-container-image. It will be passed some
# additional parameters.
"release_repo_build_and_push_images_cmd": (str, "./build-images.py"),
"docker_registry": (str, "docker-registry.discovery.wmnet"),
"mediawiki_image_name": (str, "restricted/mediawiki-multiversion"),
"mediawiki_debug_image_name": (str, "restricted/mediawiki-multiversion-debug"),
"webserver_image_name": (str, "restricted/mediawiki-webserver"),
"mediawiki_image_extra_packages": (str, ""),
# Path to a CA cert to inject into the image
"mediawiki_image_extra_ca_cert": (str, None),
"helmfile_services_dir": (str, "/srv/deployment-charts/helmfile.d/services"),
"helmfile_mediawiki_release_dir": (str, "/etc/helmfile-defaults/mediawiki/release"),
# Comma separated list of the clusters we will deploy to
"k8s_clusters": (str, "eqiad, codfw"),
"k8s_deployments_file": (str, "/etc/helmfile-defaults/mediawiki-deployments.yaml"),
"k8s_max_concurrent_deployments_per_dc": (int, 20),
"k8s_deployments_info_target_freshness": (int, 10),
# End settings related to building and deploying mediawiki container image
# Settings related to scap installation
"install_ssh_user": (str, "scap"),
"scap_source_dir": (str, "/srv/deployment/scap"),
"scap_targets": (str, "scap_targets"),
# Phabricator/Phorge settings
"phorge_url": (str, "https://phabricator.wikimedia.org"),
# Settings related to "apply-patches"
"require_security_patches": (bool, True), # T350070
"notify_patch_failures": (bool, False),
"patch_bot_phorge_name": (str, "SecurityPatchBot"),
"patch_bot_phorge_token": (str, None),
}
[docs]def load(cfg_file=None, environment=None, overrides=None, use_global_config=True):
"""
Load configuration.
A configuration file consists of sections, led by a ``[section]`` header
and followed by ``name: value`` entries. Lines beginning with ``'#'`` are
ignored and may be used to provide comments.
A configuration file can contain multiple sections. The configuration
object is populated with values from the ``global`` section and additional
sections based on the fully qualified domain name of the local host. For
example, on the host ``deployXXXX.eqiad.wmnet`` the final value for a given
setting would be the first value found in sections:
``deployXXXX.eqiad.wmnet``, ``eqiad.wmnet``, ``wmnet`` or ``global``.
Sections not present in the configuration file will be ignored.
Configuration values are loaded from a file specified by the ``-c`` or
``--conf`` command-line options or from the default locations with the
following hierarchy, sorted by override priority:
#. ``$(pwd)/scap/environments/<environment>/scap.cfg`` or
``$(pwd)/scap/scap.cfg`` (if no environment was specified)
#. ``/etc/scap.cfg`` (if use_global_config is true)
For example, if a configuration parameter is set in
``$(pwd)/scap/scap.cfg`` and that same parameter is set in
``/etc/scap.cfg`` the value for that parameter set in
``$(pwd)/scap/scap.cfg`` will be used during execution.
:param cfg_file: Alternate configuration file
:param environment: the string path under which scap.cfg is found
:param overrides: Dict of configuration values
:param use_global_config: A boolean indicating if /etc/scap.cfg should be read
:returns: dict of configuration values
"""
local_cfg = os.path.join(os.getcwd(), "scap")
parser = ConfigParser()
if cfg_file:
try:
cfg_file = open(cfg_file)
except TypeError:
# Assume that cfg_file is already an open file
pass
if hasattr(parser, "read_file"):
parser.read_file(cfg_file)
else:
parser.readfp(cfg_file)
else:
if environment and not os.path.exists(
os.path.join(local_cfg, "environments", environment)
):
raise RuntimeError("Environment {} does not exist!".format(environment))
files = []
if use_global_config:
files.append("/etc/scap.cfg")
files.extend(
[
os.path.join(local_cfg, "scap.cfg"),
utils.get_env_specific_filename(
os.path.join(local_cfg, "scap.cfg"), environment
),
]
)
parser.read(files)
fqdn = socket.getfqdn().rstrip(".").split(".")
sections = ["global"]
sections += [".".join(fqdn[x:]) for x in range(0, len(fqdn))][::-1]
config = {key: value for key, (_, value) in DEFAULT_CONFIG.items()}
for section in sections:
if parser.has_section(section):
# Do not interpolate items in the section.
# Fixes crash on deployment server:
# 'int' object has no attribute 'find'
for key, value in parser.items(section, True):
config[key] = coerce_value(key, value)
config = override_config(config, overrides)
if not environment and config.get("environment", None):
return load(cfg_file, config.get("environment"), overrides)
config["environment"] = environment
if cfg_file:
cfg_file.close()
return config
[docs]def override_config(config, overrides=None):
"""Override values in a config with type-coerced values."""
if overrides:
for key, value in overrides.items():
config[key] = coerce_value(key, value)
return config
[docs]def coerce_value(key, value):
"""Coerce the given value based on the default config type."""
if key in DEFAULT_CONFIG:
default_type, _ = DEFAULT_CONFIG[key]
if isinstance(value, default_type):
return value
if default_type == bool:
lower = value.lower()
# Accept the same bool values accepted by ConfigParser
if lower in ["1", "yes", "true", "on"]:
return True
if lower in ["0", "no", "false", "off"]:
return False
msg = "invalid boolean value '{}'".format(value)
raise ValueError(msg)
else:
return default_type(value)
return value
[docs]def multi_value(str_value):
"""
Given a string that's got commas, turn it into a list
:param str_value: Random thing the user typed in config
"""
comma_list = [x.strip() for x in str_value.split(",")]
return comma_list