mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-04 11:07:35 +01:00
* weed-vector: read the cluster's security.toml The engine finds security.toml where weed processes do (., ~/.seaweedfs, /usr/local/etc/seaweedfs, /etc/seaweedfs) with WEED_* environment variables over it, and builds gRPC client and server credentials by the rules of security.LoadClientTLS and LoadServerTLS: a dedicated client certificate is preferred, both halves of a pair must be set, and grpc.ca is required once either is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: gRPC TLS to the masters and from the gateway The masters are dialed with [grpc.client]'s credentials when the cluster has them, the master's certificate checked against the host dialed or --tls-server-name. The engine's own listener takes its identity from [grpc.vector] and then requires a client certificate chaining to grpc.ca, which is how the gateway, dialing with [grpc.client], reaches it. The rustls provider is pinned before the first TLS use, as the other binaries linking both aws-lc-rs and ring do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * scaffold: document [grpc.vector] Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * test/s3vectors: put every gRPC hop under mutual TLS in the hardened leg VECTOR_TLS=1 now also writes [grpc.master], [grpc.volume], [grpc.filer], [grpc.s3], [grpc.client] and [grpc.vector], and probes the master's and the engine's gRPC ports: plaintext HTTP/2 and a client without a certificate are refused, the client certificate gets a SETTINGS frame. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * test/s3vectors: give the test volume server 64 volume slots Every collection grows 7 volumes and test_vector_bucket_encryption opens two buckets while the previous test's bucket is still being torn down, so 16 slots were marginal: the Go legs failed CreateIndex now and then with "failed to find writable volumes" and master.log showing "create 7 volume, created 4: Not enough data nodes found!". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: reload the listener's certificates and enforce allowed_commonNames The listener now runs the way weed servers run theirs. The certificate, key and CA files are re-read every WEED_TLS_CERT_REFRESH_INTERVAL (weed's default of 5h) and on SIGHUP, and new handshakes use the changed files without a restart; a file that no longer parses is logged and the working configuration stays. [grpc.vector] allowed_commonNames and grpc.allowed_wildcard_domain restrict which client certificates are served, checked against the subject common name as security.Authenticator and the Rust volume server's CommonNameVerifier do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * scaffold: allowed_commonNames and the refresh cadence for [grpc.vector] Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * test/s3vectors: the engine's allowlist keeps an outsider certificate out The hardened leg sets [grpc.vector] allowed_commonNames = "localhost" and probes the engine with a second certificate the same CA signed for CN=outsider: the handshake must be refused, while the gateway's certificate still gets an HTTP/2 SETTINGS frame. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * weed-vector: take the master's TLS name from the parsed endpoint A master given without a port (https://master.example) had the whole URL checked against its certificate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: bound the master TLS handshake connect_timeout covers only the TCP connect, so a master that accepted the connection and never answered the handshake held up the other masters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: look up security.toml keys the way viper does An empty WEED_* variable no longer hides the file's value, and keys match regardless of case, as for weed processes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: refuse master allowlists it cannot enforce weed checks a dialed server against allowed_server_commonNames and allowed_server_wildcard_domain on top of grpc.ca. The engine's master dial accepted every certificate the CA signed even when one was set; it now fails at startup and names the key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test/s3vectors: read the whole frame header in the gRPC TLS probe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test/s3vectors: probe the volume, filer and S3 gRPC listeners too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: read the master dial credentials only when given masters A standalone engine never dials with grpc.client, so its settings no longer stop it from starting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: run each listener TLS handshake on its own task The accept loop awaited every handshake before accepting the next connection, so a peer that connected and sent nothing stopped every gateway connection after it. Handshakes now run concurrently, each under a 10s timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: read the master dial's TLS files at every dial The client certificate, key and CA were read once at startup, so after the files rotated a reconnect still presented the old pair. Each dial now reads them, as the listener re-reads its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: read every unit Go durations accept in the refresh interval WEED_TLS_CERT_REFRESH_INTERVAL values in ns, us or a leading + fell back to five hours in the engine while weed servers honored them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: fall back on a refresh interval too long to hold An oversized WEED_TLS_CERT_REFRESH_INTERVAL panicked the engine at startup; weed servers reject it and keep the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: refuse allowed_commonNames without the listener's certificate With [grpc.vector] allowed_commonNames set but no cert and key, the listener served plaintext and checked no client at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: read the master dial's credentials inside its connect timeout The CA, certificate and key were read synchronously before the timeout started, so a stalled credential path held up every other master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * weed-vector: let a set key win over an empty spelling of it With allowed_server_commonNames = "" beside allowed_server_commonnames = "master", the lookup stopped at the empty exact match and the allowlist went unnoticed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
347 lines
18 KiB
Python
347 lines
18 KiB
Python
import base64
|
|
import contextlib
|
|
import gzip
|
|
import hashlib
|
|
import hmac
|
|
import http.client
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import socket
|
|
import ssl
|
|
import subprocess
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
import uuid
|
|
import zlib
|
|
|
|
import boto3
|
|
from botocore.config import Config
|
|
import pytest
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
|
|
def combinations():
|
|
volumes = os.environ.get("VOLUME_SERVER_IMPL", "go,rust").split(",")
|
|
default_modes = "single,distributed" if (HERE / "../../rust/sw-vector-distributed/Cargo.toml").is_file() else "single"
|
|
modes = os.environ.get("VECTOR_QUERY_MODE", default_modes).split(",")
|
|
if not set(volumes) <= {"go", "rust"} or not set(modes) <= {"single", "distributed"}:
|
|
raise ValueError("invalid VOLUME_SERVER_IMPL or VECTOR_QUERY_MODE")
|
|
return [(volume, mode) for volume in volumes for mode in modes]
|
|
|
|
|
|
def binary(variable, name):
|
|
path = Path(os.environ.get(variable, str(HERE / name))).resolve()
|
|
if not path.is_file() or not os.access(path, os.X_OK):
|
|
pytest.fail(f"Missing executable {path}; run make build or set {variable}")
|
|
return str(path)
|
|
|
|
|
|
def wait_for(check, processes, message):
|
|
deadline = time.monotonic() + 60
|
|
while time.monotonic() < deadline:
|
|
for name, process in processes.items():
|
|
if process.poll() is not None:
|
|
pytest.fail(f"{name} exited with {process.returncode}; {message}")
|
|
try:
|
|
if check():
|
|
return
|
|
except (OSError, urllib.error.URLError):
|
|
pass
|
|
time.sleep(0.2)
|
|
pytest.fail(message)
|
|
|
|
|
|
def listening(port):
|
|
with socket.create_connection(("127.0.0.1", port), timeout=0.5):
|
|
return True
|
|
|
|
|
|
def http_ready(url):
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=0.5):
|
|
return True
|
|
except urllib.error.HTTPError:
|
|
return True
|
|
|
|
|
|
def stop(process):
|
|
if process.poll() is None:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=10)
|
|
|
|
|
|
@pytest.fixture(scope="module", params=combinations(), ids=lambda p: "-".join(p))
|
|
def cluster(request, tmp_path_factory):
|
|
yield from run_cluster(request.param, tmp_path_factory, identities=True)
|
|
|
|
|
|
@pytest.fixture(scope="module", params=combinations(), ids=lambda p: "-".join(p))
|
|
def open_cluster(request, tmp_path_factory):
|
|
"""A cluster with no S3 identities, and an engine given no credentials."""
|
|
yield from run_cluster(request.param, tmp_path_factory, identities=False)
|
|
|
|
|
|
def write_tls(directory):
|
|
"""A CA and one certificate for localhost. Volume servers serve HTTPS and
|
|
every gRPC listener (master, volume, filer, gateway, engine) serves TLS,
|
|
all requiring client certificates, and every client presents the same one."""
|
|
def openssl(*args):
|
|
subprocess.run(["openssl", *args], check=True, capture_output=True, cwd=directory)
|
|
openssl("req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", "ca.key", "-out", "ca.pem",
|
|
"-subj", "/CN=s3vectors test CA", "-days", "2",
|
|
"-addext", "basicConstraints=critical,CA:TRUE", "-addext", "keyUsage=critical,keyCertSign,cRLSign")
|
|
openssl("req", "-newkey", "rsa:2048", "-nodes", "-keyout", "key.pem", "-out", "req.csr", "-subj", "/CN=localhost")
|
|
(directory / "ext.cnf").write_text(
|
|
"subjectAltName=DNS:localhost,IP:127.0.0.1\nextendedKeyUsage=serverAuth,clientAuth\nbasicConstraints=CA:FALSE\n")
|
|
openssl("x509", "-req", "-in", "req.csr", "-CA", "ca.pem", "-CAkey", "ca.key", "-CAcreateserial",
|
|
"-out", "cert.pem", "-days", "2", "-extfile", "ext.cnf")
|
|
# A second identity the same CA signed, which the engine's allowlist
|
|
# must keep out.
|
|
openssl("req", "-newkey", "rsa:2048", "-nodes", "-keyout", "outsider.key", "-out", "outsider.csr", "-subj", "/CN=outsider")
|
|
openssl("x509", "-req", "-in", "outsider.csr", "-CA", "ca.pem", "-CAkey", "ca.key", "-CAcreateserial",
|
|
"-out", "outsider.pem", "-days", "2", "-extfile", "ext.cnf")
|
|
cert, key, ca = (f'{key} = {json.dumps(str(directory / name), ensure_ascii=False)}\n' for key, name in (("cert", "cert.pem"), ("key", "key.pem"), ("ca", "ca.pem")))
|
|
pem = cert + key + ca
|
|
grpc = "".join(f"[grpc.{component}]\n{cert}{key}\n" for component in ("master", "volume", "filer", "s3", "client"))
|
|
grpc += f"[grpc.vector]\n{cert}{key}allowed_commonNames = \"localhost\"\n"
|
|
(directory / "security.toml").write_text(f"[https.volume]\n{pem}\n[https.client]\nenabled = true\n{pem}\n[grpc]\n{ca}\n{grpc}")
|
|
|
|
|
|
def probe_grpc_tls(name, port, directory, outsider=False):
|
|
"""A TLS gRPC listener must refuse plaintext HTTP/2 and a connection
|
|
without the client certificate, and answer the HTTP/2 preface with it.
|
|
With `outsider`, a certificate the CA signed for a name outside the
|
|
listener's allowed_commonNames must be refused too."""
|
|
preface = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
|
with socket.create_connection(("127.0.0.1", port), timeout=5) as sock:
|
|
sock.sendall(preface)
|
|
with contextlib.suppress(ConnectionError, TimeoutError):
|
|
answer = sock.recv(16)
|
|
# Nothing, or a TLS alert record; a plaintext server sends SETTINGS.
|
|
assert not answer or answer[0] == 0x15, f"{name} gRPC answered plaintext HTTP/2: {answer!r}"
|
|
context = ssl.create_default_context(cafile=directory / "ca.pem")
|
|
context.set_alpn_protocols(["h2"])
|
|
with contextlib.suppress(ConnectionError, TimeoutError, ssl.SSLError):
|
|
with socket.create_connection(("127.0.0.1", port), timeout=5) as sock:
|
|
with context.wrap_socket(sock, server_hostname="localhost") as tls:
|
|
tls.sendall(preface)
|
|
assert not tls.recv(16), f"{name} gRPC served a client without a certificate"
|
|
if outsider:
|
|
other = ssl.create_default_context(cafile=directory / "ca.pem")
|
|
other.set_alpn_protocols(["h2"])
|
|
other.load_cert_chain(directory / "outsider.pem", directory / "outsider.key")
|
|
with contextlib.suppress(ConnectionError, TimeoutError, ssl.SSLError):
|
|
with socket.create_connection(("127.0.0.1", port), timeout=5) as sock:
|
|
with other.wrap_socket(sock, server_hostname="localhost") as tls:
|
|
tls.sendall(preface)
|
|
assert not tls.recv(16), f"{name} gRPC served a certificate outside allowed_commonNames"
|
|
context.load_cert_chain(directory / "cert.pem", directory / "key.pem")
|
|
with socket.create_connection(("127.0.0.1", port), timeout=5) as sock:
|
|
with context.wrap_socket(sock, server_hostname="localhost") as tls:
|
|
tls.sendall(preface)
|
|
header = b""
|
|
while len(header) < 9 and (chunk := tls.recv(9 - len(header))):
|
|
header += chunk
|
|
# The first server frame is SETTINGS (type 4).
|
|
assert len(header) == 9 and header[3] == 4, \
|
|
f"{name} gRPC did not answer the HTTP/2 preface over TLS: {header!r}"
|
|
|
|
|
|
def read_token(key, fid):
|
|
def encode(data):
|
|
return base64.urlsafe_b64encode(data).rstrip(b"=")
|
|
signed = encode(b'{"alg":"HS256","typ":"JWT"}') + b"." + encode(json.dumps({"fid": fid}).encode())
|
|
return (signed + b"." + encode(hmac.new(key.encode(), signed, hashlib.sha256).digest())).decode()
|
|
|
|
|
|
def probe_hardening(endpoint, filer, vol, directory, read_jwt_key):
|
|
"""The hardened switches must be enforced, not only configured: a TLS
|
|
volume server refuses plain HTTP and a connection without the client
|
|
certificate, a volume server under a read key refuses an unsigned
|
|
read of a real chunk, and an encrypted chunk is stored as ciphertext."""
|
|
scheme = "https" if os.environ.get("VECTOR_TLS") else "http"
|
|
context = None
|
|
if scheme == "https":
|
|
context = ssl.create_default_context(cafile=directory / "ca.pem")
|
|
for url, kwargs in ((f"http://127.0.0.1:{vol}/status", {}),
|
|
(f"https://127.0.0.1:{vol}/status", {"context": context})):
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=5, **kwargs) as response:
|
|
assert response.status != 200, f"{url} answered without the client certificate"
|
|
# Go answers plaintext with HTTP 400, Rust with a TLS alert.
|
|
except (urllib.error.URLError, ConnectionError, ssl.SSLError, http.client.HTTPException):
|
|
pass
|
|
context.load_cert_chain(directory / "cert.pem", directory / "key.pem")
|
|
with urllib.request.urlopen(f"https://127.0.0.1:{vol}/status", timeout=5, context=context) as response:
|
|
assert response.status == 200
|
|
encrypted = os.environ.get("VECTOR_ENCRYPT_VOLUMES")
|
|
if not (read_jwt_key or encrypted):
|
|
return
|
|
s3 = boto3.client("s3", endpoint_url=endpoint, aws_access_key_id="test", aws_secret_access_key="test",
|
|
region_name="us-east-1", config=Config(s3={"addressing_style": "path"}))
|
|
bucket = "hardening-" + uuid.uuid4().hex[:12]
|
|
body = b"x" * 1024
|
|
s3.create_bucket(Bucket=bucket)
|
|
try:
|
|
s3.put_object(Bucket=bucket, Key="chunk", Body=body)
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{filer}/buckets/{bucket}/chunk?metadata=true", timeout=5) as response:
|
|
chunk = json.load(response)["chunks"][0]
|
|
url = f"{scheme}://127.0.0.1:{vol}/{chunk['file_id']}"
|
|
headers = {}
|
|
if read_jwt_key:
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=5, context=context):
|
|
raise AssertionError("the volume server served an unsigned read")
|
|
except urllib.error.HTTPError as error:
|
|
assert error.code == 401, error
|
|
headers["Authorization"] = "Bearer " + read_token(read_jwt_key, chunk["file_id"])
|
|
with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=5, context=context) as response:
|
|
stored = response.read()
|
|
if encrypted:
|
|
assert chunk.get("cipher_key"), "the gateway stored a chunk without a cipher key"
|
|
assert stored and stored != body, "the volume server holds the chunk in plaintext or not at all"
|
|
with contextlib.suppress(OSError, EOFError, zlib.error):
|
|
assert gzip.decompress(stored) != body, "the volume server holds the chunk gzipped, not encrypted"
|
|
else:
|
|
assert stored == body, "the signed read did not return the chunk"
|
|
finally:
|
|
s3.delete_object(Bucket=bucket, Key="chunk")
|
|
s3.delete_bucket(Bucket=bucket)
|
|
|
|
|
|
def run_cluster(param, tmp_path_factory, identities):
|
|
volume, mode = param
|
|
directory = tmp_path_factory.mktemp(f"vectors-{volume}-{mode}" + ("" if identities else "-open"))
|
|
print(f"\nVector integration logs: {directory}")
|
|
weed = binary("WEED_BINARY", "weed")
|
|
engine = binary("VECTOR_BINARY", "weed-vector")
|
|
worker = binary("VECTOR_WORKER_BINARY", "weed-vector-worker") if mode == "distributed" else None
|
|
volume_binary = binary("RUST_VOLUME_SERVER_BINARY", "weed-volume") if volume == "rust" else weed
|
|
processes = {}
|
|
with contextlib.ExitStack() as stack:
|
|
with contextlib.ExitStack() as reservations:
|
|
ports = []
|
|
for _ in range(12):
|
|
sock = reservations.enter_context(socket.socket())
|
|
sock.bind(("127.0.0.1", 0))
|
|
ports.append(sock.getsockname()[1])
|
|
master, master_grpc, vol, vol_grpc, filer, filer_grpc, s3, s3_grpc, lance, vector, worker1, worker2 = ports
|
|
master_addr = f"127.0.0.1:{master}.{master_grpc}"
|
|
filer_addr = f"127.0.0.1:{filer}.{filer_grpc}"
|
|
|
|
# VECTOR_READ_JWT=1 makes volume reads need a JWT; every weed process
|
|
# and the workers sign with the key.
|
|
read_jwt_key = os.environ.get("VECTOR_READ_JWT") and uuid.uuid4().hex
|
|
# VECTOR_TLS=1 puts the volume servers behind HTTPS with client
|
|
# certificates; every process finds security.toml in its cwd.
|
|
if os.environ.get("VECTOR_TLS"):
|
|
write_tls(directory)
|
|
|
|
def start(name, args, unset=()):
|
|
log = stack.enter_context((directory / f"{name}.log").open("w"))
|
|
env = dict(os.environ, AWS_ACCESS_KEY_ID="test", AWS_SECRET_ACCESS_KEY="test",
|
|
AWS_DEFAULT_REGION="us-east-1", AWS_EC2_METADATA_DISABLED="true",
|
|
WEED_S3_SSE_KEY="s3vectors-test",
|
|
**({"WEED_JWT_SIGNING_READ_KEY": read_jwt_key} if read_jwt_key else {}))
|
|
env.pop("WEED_S3_SSE_KEK", None)
|
|
if not read_jwt_key:
|
|
env.pop("WEED_JWT_SIGNING_READ_KEY", None)
|
|
for key in unset:
|
|
env.pop(key)
|
|
process = subprocess.Popen(args, cwd=directory, stdout=log, stderr=log, env=env)
|
|
processes[name] = process
|
|
stack.callback(stop, process)
|
|
return process
|
|
|
|
for name in ("master", "volume", "filer"):
|
|
(directory / name).mkdir()
|
|
start("master", [weed, "-v=1", "master", "-ip=127.0.0.1", "-ip.bind=127.0.0.1",
|
|
f"-port={master}", f"-port.grpc={master_grpc}",
|
|
f"-mdir={directory / 'master'}", "-peers=none", "-volumeSizeLimitMB=32"])
|
|
wait_for(lambda: http_ready(f"http://127.0.0.1:{master}/dir/status"), processes, "master not ready")
|
|
prefix = [volume_binary] if volume == "rust" else [weed, "volume"]
|
|
flag = "--" if volume == "rust" else "-"
|
|
start("volume", prefix + [f"{flag}{key}={value}" for key, value in {
|
|
"master": master_addr, "ip": "127.0.0.1", "ip.bind": "127.0.0.1",
|
|
"port": vol, "port.grpc": vol_grpc, "dir": directory / "volume",
|
|
"max": 64, "preStopSeconds": 0,
|
|
}.items()])
|
|
wait_for(lambda: listening(vol_grpc), processes, "volume not ready")
|
|
start("filer", [weed, "filer", f"-master={master_addr}", "-ip=127.0.0.1",
|
|
"-ip.bind=127.0.0.1", f"-port={filer}", f"-port.grpc={filer_grpc}",
|
|
f"-defaultStoreDir={directory / 'filer'}"])
|
|
wait_for(lambda: listening(filer_grpc), processes, "filer not ready")
|
|
endpoint = f"http://127.0.0.1:{s3}"
|
|
# weed s3 turns AWS_ACCESS_KEY_ID into an admin identity, and the engine
|
|
# would find it as a credential.
|
|
if identities:
|
|
config, unset = [f"-config={HERE / 's3.json'}"], ()
|
|
credentials = ["--storage-option", "aws_access_key_id=test",
|
|
"--storage-option", "aws_secret_access_key=test"]
|
|
else:
|
|
config, unset, credentials = [], ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"), []
|
|
# VECTOR_ENCRYPT_VOLUMES=1 has the gateway encrypt every chunk it
|
|
# stores, which direct reads must undo.
|
|
encrypt = ["-encryptVolumeData"] if os.environ.get("VECTOR_ENCRYPT_VOLUMES") else []
|
|
start("s3", [weed, "-v=1", "s3", f"-filer={filer_addr}", *config, *encrypt,
|
|
"-ip=127.0.0.1", "-ip.bind=127.0.0.1", f"-port={s3}",
|
|
f"-port.grpc={s3_grpc}", "-port.iceberg=0", f"-port.lance={lance}"], unset)
|
|
wait_for(lambda: http_ready(f"http://127.0.0.1:{lance}/v1/namespace/root/list"), processes, "catalog not ready")
|
|
if os.environ.get("VECTOR_TLS") or read_jwt_key or encrypt:
|
|
probe_hardening(endpoint, filer, vol, directory, read_jwt_key)
|
|
start("vector", [engine, "--namespace", f"http://127.0.0.1:{lance}",
|
|
"--masters", master_addr, "--listen", f"127.0.0.1:{vector}", *credentials], unset)
|
|
wait_for(lambda: listening(vector), processes, "vector engine not ready")
|
|
if os.environ.get("VECTOR_TLS"):
|
|
wait_for(lambda: listening(s3_grpc), processes, "s3 gRPC not ready")
|
|
for name, port in (("master", master_grpc), ("volume", vol_grpc), ("filer", filer_grpc),
|
|
("s3", s3_grpc)):
|
|
probe_grpc_tls(name, port, directory)
|
|
probe_grpc_tls("engine", vector, directory, outsider=True)
|
|
|
|
wait_for(
|
|
lambda: f"Cluster: vector node 127.0.0.1:{vector} added to group ''"
|
|
in (directory / "master.log").read_text(),
|
|
processes,
|
|
"vector engine never registered with the master",
|
|
)
|
|
|
|
if worker:
|
|
for i, port in enumerate((worker1, worker2)):
|
|
start(f"worker-{i}", [worker, "--masters", master_addr, "--listen", f"127.0.0.1:{port}"])
|
|
wait_for(lambda: listening(port), processes, "vector worker not ready")
|
|
wait_for(
|
|
lambda: all(
|
|
f"S3 Vectors: discovered search worker 127.0.0.1:{port}"
|
|
in (directory / "s3.log").read_text()
|
|
for port in (worker1, worker2)
|
|
),
|
|
processes,
|
|
"gateway did not discover both search workers",
|
|
)
|
|
yield {"endpoint": endpoint, "mode": mode, "processes": processes, "directory": directory}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def vectors(cluster):
|
|
return boto3.client("s3vectors", endpoint_url=cluster["endpoint"],
|
|
aws_access_key_id="test", aws_secret_access_key="test", region_name="us-east-1",
|
|
config=Config(connect_timeout=3, read_timeout=20, retries={"max_attempts": 0}))
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def vector_clients(vectors):
|
|
return {
|
|
name: boto3.client("s3vectors", endpoint_url=vectors.meta.endpoint_url,
|
|
aws_access_key_id=name, aws_secret_access_key=name + "-secret",
|
|
region_name="us-east-1", config=vectors.meta.config)
|
|
for name in ("reader", "writer")
|
|
}
|