Files
scylladb/test/nodetool/test_ring.py
Avi Kivity f3eade2f62 treewide: relicense to ScyllaDB-Source-Available-1.0
Drop the AGPL license in favor of a source-available license.
See the blog post [1] for details.

[1] https://www.scylladb.com/2024/12/18/why-were-moving-to-a-source-available-license/
2024-12-18 17:45:13 +02:00

249 lines
9.1 KiB
Python

#
# Copyright 2023-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
#
from typing import NamedTuple
import functools
import operator
from socket import getnameinfo
import pytest
from test.nodetool.rest_api_mock import expected_request
from test.nodetool.utils import format_size, check_nodetool_fails_with
null_ownership_error = ("Non-system keyspaces don't have the same replication settings, "
"effective ownership information is meaningless")
class Host(NamedTuple):
dc: str
rack: str
endpoint: str
status: str
state: str
load: float
ownership: float
tokens: list[str]
def token_to_endpoint(self):
return {
token: self.endpoint for token in self.tokens
}
def get_address(self, resolve_ip):
if resolve_ip:
host, _ = getnameinfo((self.endpoint, 0), 0)
return host
return self.endpoint
def map_to_json(mapper, mapped_type=None):
json = []
for key, value in mapper.items():
if mapped_type is None:
json.append({'key': key, 'value': value})
else:
json.append({'key': key, 'value': mapped_type(value)})
return json
def format_stat(width, address, rack, status, state, load, owns, token):
return f"{address:<{width}} {rack:<12}{status:<7}{state:<8}{load:<16}{owns:<20}{token:<44}\n"
@pytest.mark.parametrize("keyspace_table,resolve_ip,host_status,host_state",
[
('ks', '', 'live', 'joining'),
('ks.table', '', 'live', 'normal'),
('ks', '', 'live', 'leaving'),
('ks.table', '', 'live', 'moving'),
('ks', '', 'down', 'n/a'),
('', '', 'live', 'normal'),
('', '-r', 'live', 'normal'),
('', '--resolve-ip', 'live', 'normal'),
])
def test_ring(request, nodetool, keyspace_table, resolve_ip, host_status, host_state):
uses_cassandra_nodetool = request.config.getoption("nodetool") == "cassandra"
if "." in keyspace_table:
keyspace, table = keyspace_table.split(".")
else:
keyspace, table = keyspace_table, None
if uses_cassandra_nodetool and table is not None:
pytest.skip("skipping tablets-related test with Cassandra nodetool")
host = Host('dc0', 'rack0', '127.0.0.1', host_status, host_state,
6414780.0, 1.0,
["-9217327499541836964",
"9066719992055809912",
"50927788561116407"])
endpoint_to_ownership = {
host.endpoint: host.ownership,
}
all_hosts = [host]
token_to_endpoint = functools.reduce(operator.or_,
(h.token_to_endpoint() for h in all_hosts))
def hosts_in_status(status):
return list(h.endpoint for h in all_hosts if h.status == status)
def hosts_in_state(state):
return list(h.endpoint for h in all_hosts if h.state == state)
status_to_endpoints = dict(
(status, hosts_in_status(status)) for status in ['live', 'down'])
state_to_endpoints = dict(
(state, hosts_in_state(state)) for state in ['joining', 'leaving', 'moving'])
load_map = dict((h.endpoint, h.load) for h in all_hosts)
expected_requests = []
if keyspace != '' and table is None and not uses_cassandra_nodetool:
expected_requests.append(expected_request("GET", "/storage_service/keyspaces",
params={"replication": "tablets"},
multiple=expected_request.ONE, response=[]))
tokens_endpoint_params = {}
if table is not None:
tokens_endpoint_params["keyspace"] = keyspace
tokens_endpoint_params["cf"] = table
expected_requests += [
expected_request('GET', '/storage_service/tokens_endpoint', params=tokens_endpoint_params,
response=map_to_json(token_to_endpoint))
]
is_scylla = request.config.getoption("nodetool") == "scylla"
print_all_keyspaces = keyspace == ''
if is_scylla and print_all_keyspaces:
# scylla nodetool does not bother getting ownership if keyspace is not
# specified
pass
else:
expected_requests.append(
expected_request(
"GET",
"/storage_service/ownership/null",
response_status=500,
multiple=expected_request.ANY,
response={"message": f"std::runtime_error({null_ownership_error})", "code": 500}))
params = {}
if table is not None:
params["cf"] = table
expected_requests.append(
expected_request('GET', f'/storage_service/ownership/{keyspace}', params=params,
response=map_to_json(endpoint_to_ownership, str)))
expected_requests += [
expected_request('GET', '/snitch/datacenter',
params={'host': host.endpoint},
multiple=expected_request.ANY,
response=host.dc),
expected_request('GET', '/gossiper/endpoint/live',
response=status_to_endpoints['live']),
expected_request('GET', '/gossiper/endpoint/down',
response=status_to_endpoints['down']),
expected_request('GET', '/storage_service/nodes/joining',
response=state_to_endpoints['joining']),
expected_request('GET', '/storage_service/nodes/leaving',
response=state_to_endpoints['leaving']),
expected_request('GET', '/storage_service/nodes/moving',
response=state_to_endpoints['moving']),
expected_request('GET', '/storage_service/load_map',
response=map_to_json(load_map)),
expected_request('GET', '/snitch/rack',
params={'host': host.endpoint},
multiple=expected_request.ANY,
response=host.rack),
]
args = []
if keyspace:
args.append(keyspace)
if table:
args.append(table)
if resolve_ip:
args.append(resolve_ip)
res = nodetool('ring', *args, expected_requests=expected_requests)
actual_output = res.stdout
expected_output = f'''
Datacenter: {host.dc}
==========
'''
max_width = max(len(h.get_address(resolve_ip)) for h in all_hosts)
last_token = list(token_to_endpoint)[-1]
expected_output += format_stat(max_width, 'Address', 'Rack',
'Status', 'State', 'Load', 'Owns', 'Token')
expected_output += format_stat(max_width, '', '',
'', '', '', '', last_token)
have_vnode = False
all_endpoints = set()
for token, endpoint in token_to_endpoint.items():
assert host.endpoint == endpoint
if endpoint in all_endpoints:
have_vnode = True
all_endpoints.add(endpoint)
if endpoint in status_to_endpoints['live']:
status = 'Up'
elif endpoint in status_to_endpoints['down']:
status = 'Down'
else:
status = '?'
if endpoint in state_to_endpoints['joining']:
state = 'Joining'
elif endpoint in state_to_endpoints['leaving']:
state = 'Leaving'
elif endpoint in state_to_endpoints['moving']:
state = 'Moving'
else:
state = 'Normal'
load = format_size(host.load)
if print_all_keyspaces:
ownership = '?'
else:
# scylla nodetool always prints out the ownership percentage,
# since it prints out the warning, so user is aware if the
# ownership is meaningless or not
ownership_percent = host.ownership * 100
ownership = f'{ownership_percent:.2f}%'
expected_output += format_stat(max_width, host.get_address(resolve_ip),
host.rack, status, state, load,
ownership, token)
expected_output += '\n'
if have_vnode:
expected_output += '''\
Warning: "nodetool ring" is used to output all the tokens of a node.
To view status related info of a node use "nodetool status" instead.
'''
warning = ''
if print_all_keyspaces:
warning = '''Note: Non-system keyspaces don't have the same replication settings, effective ownership information is meaningless\n'''
expected_output += f'''\
{warning}'''
assert actual_output == expected_output
def test_ring_tablet_keyspace_no_table(nodetool, scylla_only):
keyspace = "ks"
check_nodetool_fails_with(
nodetool,
("ring", keyspace),
{"expected_requests": [
expected_request("GET", "/storage_service/keyspaces", params={"replication": "tablets"},
multiple=expected_request.ONE, response=[keyspace])]},
["error processing arguments: need a table to obtain ring for tablet keyspace"])