Files
scylladb/test/nodetool/test_ring.py
Botond Dénes e82455beab tools/scylla-nodetool: add tablet support to ring command
Add a table parameter. Pass both keyspace and table (when provided) to
the /storage_service/tokens_endpoint API endpoint, so that the returned
(and printed) token ring is that of the table's tablets, not the vnode
ring.
Also pass the table param to the ownership API, which will complain if
this param is missing for a tablet keyspace.
2024-05-13 07:09:20 -04:00

249 lines
9.1 KiB
Python

#
# Copyright 2023-present ScyllaDB
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
from typing import NamedTuple
import functools
import operator
from socket import getnameinfo
import pytest
from rest_api_mock import expected_request
from 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"])