Files
scylladb/dist/common/scripts/scylla-housekeeping
Avi Kivity 8e480110c2 dist: housekeeping: set python.multiprocessing fork mode to "fork"
Python 3.14 changed the multiprocessing fork mode to "forkserver",
presumably for good reasons. However, it conflicts with our
relocatable Python system. "forkserver" forks and execs a Python
process at startup, but it does this without supplying our relocated
ld.so. The system ld.so detects a conflict and crashes.

Fix this by switching back to "fork", which is sufficient for
housekeeping's modest needs.

Closes scylladb/scylladb#26831
2025-11-05 15:47:38 +03:00

214 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016-present ScyllaDB
#
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
#
import argparse
import json
import os
import sys
import subprocess
import configparser
import uuid
import re
import glob
import urllib.request
from pkg_resources import parse_version
import multiprocessing as mp
# Python 3.14 changed the default to 'forkserver', which is not compatible
# with our relocatable python. It execs our Python binary, but without our
# ld.so. Change it back to 'fork' to avoid issues.
mp.set_start_method('fork')
VERSION = "1.0"
quiet = False
# Temporary url for the review
version_url = "https://i6a5h9l1kl.execute-api.us-east-1.amazonaws.com/prod/check_version"
def trace(*vals):
print(''.join(vals))
def traceln(*vals):
trace(*(vals + ('\n',)))
def help(args):
parser.print_help()
def sh_command(*args):
p = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = p.communicate()
if err:
raise Exception(err)
return out
def get_url(path):
# If server returns any error, like 403, or 500 urllib.request throws exception, which is not serializable.
# When multiprocessing routines fail to serialize it, it throws ambiguous serialization exception
# from get_json_from_url.
# In order to see legit error we catch it from the inside of process, convert to string and
# pass it as part of return value
try:
return 0, urllib.request.urlopen(path).read().decode('utf-8')
except Exception as exc:
return 1, str(exc)
def get_json_from_url(path):
pool = mp.Pool(processes=1)
# Unfortunately the timeout parameter to urlopen seems to be something internal and is not close or
# near a wallclock timeout. In my experiments, I could see a factor of up to 4, with a 5-second timeout
# passed to urlopen timing out in 20s, which creates a bad user experience. We will then use multiprocessing
# to enforce a wallclock timeout.
result = pool.apply_async(get_url, args=(path,))
try:
status, retval = result.get(timeout=5)
except mp.TimeoutError as err:
pool.terminate()
pool.join()
raise
if status == 1:
raise RuntimeError(f'Failed to get "{path}" due to the following error: {retval}')
return json.loads(retval)
def get_api(path):
return get_json_from_url("http://" + api_address + path)
def parse_scylla_version(version):
# Newer setuptools does not accept ~dev in version strings, need to
# replace it to X.Y.Z.dev0
if version and version.endswith('~dev'):
version = version.replace('~dev', '.dev0')
# Newer setuptools does not accept ~rcN in version strings, need to
# replace it to X.Y.ZrcN
if version and '~rc' in version:
version = version.replace('~', '')
return parse_version(version)
def version_compare(a, b):
return parse_scylla_version(a) < parse_scylla_version(b)
def create_uuid_file(fl):
with open(args.uuid_file, 'w') as myfile:
myfile.write(str(uuid.uuid1()) + "\n")
os.chmod(args.uuid_file, 0o644)
def sanitize_version(version):
"""
Newer setuptools don't like dashed version strings, trim it to avoid
false negative version_compare() checks.
"""
if version and '-' in version:
return version.split('-', 1)[0]
else:
return version
def get_repo_file(dir):
files = glob.glob(dir)
files.sort(key=os.path.getmtime, reverse=True)
for name in files:
with open(name, 'r') as myfile:
for line in myfile:
match = re.search(r".*http.?://repositories.*/scylladb/([^/\s]+)/.*/([^/\s]+)/scylladb-.*", line)
if match:
return match.group(2), match.group(1)
return None, None
def check_version(ar):
if config and (not config.has_option("housekeeping", "check-version") or not config.getboolean("housekeeping", "check-version")):
return
if ar.version and ar.version != '':
current_version = sanitize_version(ar.version)
else:
current_version = sanitize_version(get_api('/storage_service/scylla_release_version'))
if current_version == "":
# API is down, nothing to do
return
try:
params = "?version=" + current_version
if ar.mode:
# mode would accept any string.
# use i for install, c (default) for running from the command line
params = params + "&sts=" + ar.mode
if uid:
params = params + "&uu=" + uid
if repo_id:
params = params + "&rid=" + repo_id
if repo_type:
params = params + "&rtype=" + repo_type
versions = get_json_from_url(version_url + params)
latest_version = versions["version"]
latest_patch_version = versions["latest_patch_version"]
except Exception:
traceln("Unable to retrieve version information")
return
if latest_patch_version != latest_version:
# user is using an older minor version
if version_compare(current_version, latest_patch_version):
# user is also running an older patch version
traceln("Your current Scylla release is " + current_version + ", while the latest patch release is " + latest_patch_version +
", and the latest minor release is " + latest_version + " (recommended)")
else:
traceln("Your current Scylla release is ", current_version, " the latest minor release is ", latest_version, " go to http://www.scylladb.com for upgrade instructions")
elif version_compare(current_version, latest_patch_version):
traceln("Your current Scylla release is ", current_version, " while the latest patch release is ", latest_patch_version, ", update for the latest bug fixes and improvements")
parser = argparse.ArgumentParser(description='ScyllaDB help report tool', conflict_handler="resolve")
parser.add_argument('-q', '--quiet', action='store_true', default=False, help='Quiet mode')
parser.add_argument('-c', '--config', default="", help='An optional config file. Specifying a missing file will terminate the script')
parser.add_argument('--uuid', default="", help='A uuid for the requests')
parser.add_argument('--uuid-file', default="", help='A uuid file for the requests')
parser.add_argument('--repo-files', default="", help='The repository files that is been used for private repositories')
parser.add_argument('--api-address', default="localhost:10000", help='The ip and port of the scylla api')
subparsers = parser.add_subparsers(help='Available commands')
parser_help = subparsers.add_parser('help', help='Display help information')
parser_help.set_defaults(func=help)
parser_system = subparsers.add_parser('version', help='Check if the current running version is the latest one')
parser_system.add_argument('--mode', default="c", help='Which mode the version check runs')
parser_system.add_argument('--version', default="", help='Use a given version to compare to')
parser_system.set_defaults(func=check_version)
args = parser.parse_args()
quiet = args.quiet
config = None
repo_id = None
repo_type = None
if args.config != "":
if not os.path.isfile(args.config):
traceln("Config file ", args.config, " is missing, terminating")
sys.exit(0)
config = configparser.ConfigParser()
config.read(args.config)
uid = None
if args.uuid != "":
uid = args.uuid
if args.uuid_file != "":
if not os.path.exists(args.uuid_file):
create_uuid_file(args.uuid_file)
with open(args.uuid_file, 'r') as myfile:
uid = myfile.read().replace('\n', '')
api_address = args.api_address
if args.repo_files != "":
repo_type, repo_id = get_repo_file(args.repo_files)
args.func(args)