import logging
import random
-from typing import List, Optional, Callable, Iterable, TypeVar, Set
+from typing import List, Optional, Callable, TypeVar, Set, Tuple
import orchestrator
from ceph.deployment.service_spec import HostPlacementSpec, ServiceSpec
f'hosts for label {self.spec.placement.label}')
def place(self):
- # type: () -> List[HostPlacementSpec]
+ # type: () -> Tuple[List[HostPlacementSpec], List[HostPlacementSpec], List[orchestrator.DaemonDescription]]
"""
Generate a list of HostPlacementSpec taking into account:
# get candidate hosts based on [hosts, label, host_pattern]
candidates = self.get_candidates() # type: List[HostPlacementSpec]
- if not candidates:
- return [] # sigh
- # If we don't have <count> the list of candidates is definitive.
+ # consider enough slots to fulfill target count-per-host or count
if count is None:
- logger.debug('Provided hosts: %s' % candidates)
if self.spec.placement.count_per_host:
per_host = self.spec.placement.count_per_host
else:
per_host = 1
- return candidates * per_host
-
- if self.allow_colo:
- # consider enough slots to fit target count
+ candidates = candidates * per_host
+ elif self.allow_colo and candidates:
per_host = 1 + ((count - 1) // len(candidates))
candidates = candidates * per_host
# sort candidates into existing/used slots that already have a
# daemon, and others (the rest)
- existing_active: List[HostPlacementSpec] = []
- existing_standby: List[HostPlacementSpec] = []
- existing: List[HostPlacementSpec] = []
- others = candidates
+ existing_active: List[orchestrator.DaemonDescription] = []
+ existing_standby: List[orchestrator.DaemonDescription] = []
+ existing_slots: List[HostPlacementSpec] = []
+ to_remove: List[orchestrator.DaemonDescription] = []
+ others = candidates.copy()
for d in daemons:
hs = d.get_host_placement()
+ found = False
for i in others:
if i == hs:
others.remove(i)
if d.is_active:
- existing_active.append(hs)
+ existing_active.append(d)
else:
- existing_standby.append(hs)
- existing.append(hs)
+ existing_standby.append(d)
+ existing_slots.append(hs)
+ found = True
break
- logger.debug('Hosts with existing active daemons: {}'.format(existing_active))
- logger.debug('Hosts with existing standby daemons: {}'.format(existing_standby))
+ if not found:
+ to_remove.append(d)
+
+ existing = existing_active + existing_standby
+
+ # If we don't have <count> the list of candidates is definitive.
+ if count is None:
+ logger.debug('Provided hosts: %s' % candidates)
+ return candidates, others, to_remove
# The number of new slots that need to be selected in order to fulfill count
need = count - len(existing)
# we don't need any additional placements
if need <= 0:
- del existing[count:]
- return existing
+ to_remove.extend(existing[count:])
+ del existing_slots[count:]
+ return existing_slots, [], to_remove
# ask the scheduler to select additional slots
- chosen = self.scheduler.place(others, need)
+ to_add = self.scheduler.place(others, need)
logger.debug('Combine hosts with existing daemons %s + new hosts %s' % (
- existing, chosen))
- return existing + chosen
+ existing, to_add))
+ return existing_slots + to_add, to_add, to_remove
def add_daemon_hosts(self, host_pool: List[HostPlacementSpec]) -> List[HostPlacementSpec]:
hosts_with_daemons = {d.hostname for d in self.daemons}
random.Random(seed).shuffle(hosts)
return hosts
-
-
-def merge_hostspecs(
- lh: List[HostPlacementSpec],
- rh: List[HostPlacementSpec]
-) -> Iterable[HostPlacementSpec]:
- """
- Merge two lists of HostPlacementSpec by hostname. always returns `lh` first.
-
- >>> list(merge_hostspecs([HostPlacementSpec(hostname='h', name='x', network='')],
- ... [HostPlacementSpec(hostname='h', name='y', network='')]))
- [HostPlacementSpec(hostname='h', network='', name='x')]
-
- """
- lh_names = {h.hostname for h in lh}
- yield from lh
- yield from (h for h in rh if h.hostname not in lh_names)
-
-
-def difference_hostspecs(
- lh: List[HostPlacementSpec],
- rh: List[HostPlacementSpec]
-) -> List[HostPlacementSpec]:
- """
- returns lh "minus" rh by hostname.
-
- >>> list(difference_hostspecs([HostPlacementSpec(hostname='h1', name='x', network=''),
- ... HostPlacementSpec(hostname='h2', name='y', network='')],
- ... [HostPlacementSpec(hostname='h2', name='', network='')]))
- [HostPlacementSpec(hostname='h1', network='', name='x')]
-
- """
- rh_names = {h.hostname for h in rh}
- return [h for h in lh if h.hostname not in rh_names]
except IndexError:
try:
spec = mk_spec()
- host_res = HostAssignment(
+ host_res, to_add, to_remove = HostAssignment(
spec=spec,
hosts=hosts,
daemons=daemons,
for _ in range(10): # scheduler has a random component
try:
spec = mk_spec()
- host_res = HostAssignment(
+ host_res, to_add, to_remove = HostAssignment(
spec=spec,
hosts=hosts,
daemons=daemons
hosts: List[str]
daemons: List[DaemonDescription]
expected: List[str]
+ expected_add: List[str]
+ expected_remove: List[DaemonDescription]
-@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected",
+@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
[ # noqa: E128
# just hosts
NodeAssignmentTest(
PlacementSpec(hosts=['smithi060:[v2:172.21.15.60:3301,v1:172.21.15.60:6790]=c']),
['smithi060'],
[],
- ['smithi060']
+ ['smithi060'], ['smithi060'], []
),
# all_hosts
NodeAssignmentTest(
DaemonDescription('mgr', 'a', 'host1'),
DaemonDescription('mgr', 'b', 'host2'),
],
- ['host1', 'host2', 'host3']
+ ['host1', 'host2', 'host3'], ['host3'], []
),
# all_hosts + count_per_host
NodeAssignmentTest(
DaemonDescription('mds', 'a', 'host1'),
DaemonDescription('mds', 'b', 'host2'),
],
- ['host1', 'host2', 'host3', 'host1', 'host2', 'host3']
+ ['host1', 'host2', 'host3', 'host1', 'host2', 'host3'],
+ ['host3', 'host1', 'host2', 'host3'],
+ []
),
# count that is bigger than the amount of hosts. Truncate to len(hosts)
# mgr should not be co-located to each other.
PlacementSpec(count=4),
'host1 host2 host3'.split(),
[],
- ['host1', 'host2', 'host3']
+ ['host1', 'host2', 'host3'], ['host1', 'host2', 'host3'], []
),
# count that is bigger than the amount of hosts; wrap around.
NodeAssignmentTest(
PlacementSpec(count=6),
'host1 host2 host3'.split(),
[],
- ['host1', 'host2', 'host3', 'host1', 'host2', 'host3']
+ ['host1', 'host2', 'host3', 'host1', 'host2', 'host3'],
+ ['host1', 'host2', 'host3', 'host1', 'host2', 'host3'],
+ []
),
# count + partial host list
NodeAssignmentTest(
DaemonDescription('mgr', 'a', 'host1'),
DaemonDescription('mgr', 'b', 'host2'),
],
- ['host3']
+ ['host3'], ['host3'], ['mgr.a', 'mgr.b']
),
# count + partial host list (with colo)
NodeAssignmentTest(
DaemonDescription('mgr', 'a', 'host1'),
DaemonDescription('mgr', 'b', 'host2'),
],
- ['host3', 'host3', 'host3']
+ ['host3', 'host3', 'host3'], ['host3', 'host3', 'host3'], ['mgr.a', 'mgr.b']
),
# count 1 + partial host list
NodeAssignmentTest(
DaemonDescription('mgr', 'a', 'host1'),
DaemonDescription('mgr', 'b', 'host2'),
],
- ['host3']
+ ['host3'], ['host3'], ['mgr.a', 'mgr.b']
),
# count + partial host list + existing
NodeAssignmentTest(
[
DaemonDescription('mgr', 'a', 'host1'),
],
- ['host3']
+ ['host3'], ['host3'], ['mgr.a']
),
# count + partial host list + existing (deterministic)
NodeAssignmentTest(
[
DaemonDescription('mgr', 'a', 'host1'),
],
- ['host1']
+ ['host1'], [], []
),
# count + partial host list + existing (deterministic)
NodeAssignmentTest(
[
DaemonDescription('mgr', 'a', 'host2'),
],
- ['host1']
+ ['host1'], ['host1'], ['mgr.a']
),
# label only
NodeAssignmentTest(
PlacementSpec(label='foo'),
'host1 host2 host3'.split(),
[],
- ['host1', 'host2', 'host3']
+ ['host1', 'host2', 'host3'], ['host1', 'host2', 'host3'], []
),
# label + count (truncate to host list)
NodeAssignmentTest(
PlacementSpec(count=4, label='foo'),
'host1 host2 host3'.split(),
[],
- ['host1', 'host2', 'host3']
+ ['host1', 'host2', 'host3'], ['host1', 'host2', 'host3'], []
),
# label + count (with colo)
NodeAssignmentTest(
PlacementSpec(count=6, label='foo'),
'host1 host2 host3'.split(),
[],
- ['host1', 'host2', 'host3', 'host1', 'host2', 'host3']
+ ['host1', 'host2', 'host3', 'host1', 'host2', 'host3'],
+ ['host1', 'host2', 'host3', 'host1', 'host2', 'host3'],
+ []
),
# label only + count_per_hst
NodeAssignmentTest(
'host1 host2 host3'.split(),
[],
['host1', 'host2', 'host3', 'host1', 'host2', 'host3',
- 'host1', 'host2', 'host3']
+ 'host1', 'host2', 'host3'],
+ ['host1', 'host2', 'host3', 'host1', 'host2', 'host3',
+ 'host1', 'host2', 'host3'],
+ []
),
# host_pattern
NodeAssignmentTest(
PlacementSpec(host_pattern='mgr*'),
'mgrhost1 mgrhost2 datahost'.split(),
[],
- ['mgrhost1', 'mgrhost2']
+ ['mgrhost1', 'mgrhost2'], ['mgrhost1', 'mgrhost2'], []
),
# host_pattern + count_per_host
NodeAssignmentTest(
PlacementSpec(host_pattern='mds*', count_per_host=3),
'mdshost1 mdshost2 datahost'.split(),
[],
- ['mdshost1', 'mdshost2', 'mdshost1', 'mdshost2', 'mdshost1', 'mdshost2']
+ ['mdshost1', 'mdshost2', 'mdshost1', 'mdshost2', 'mdshost1', 'mdshost2'],
+ ['mdshost1', 'mdshost2', 'mdshost1', 'mdshost2', 'mdshost1', 'mdshost2'],
+ []
),
])
-def test_node_assignment(service_type, placement, hosts, daemons, expected):
+def test_node_assignment(service_type, placement, hosts, daemons,
+ expected, expected_add, expected_remove):
service_id = None
allow_colo = False
if service_type == 'rgw':
service_id=service_id,
placement=placement)
- hosts = HostAssignment(
+ all_slots, to_add, to_remove = HostAssignment(
spec=spec,
hosts=[HostSpec(h, labels=['foo']) for h in hosts],
daemons=daemons,
allow_colo=allow_colo
).place()
- assert sorted([h.hostname for h in hosts]) == sorted(expected)
+
+ got = [hs.hostname for hs in all_slots]
+ num_wildcard = 0
+ for i in expected:
+ if i == '*':
+ num_wildcard += 1
+ else:
+ got.remove(i)
+ assert num_wildcard == len(got)
+
+ got = [hs.hostname for hs in to_add]
+ num_wildcard = 0
+ for i in expected_add:
+ if i == '*':
+ num_wildcard += 1
+ else:
+ got.remove(i)
+ assert num_wildcard == len(got)
+
+ assert sorted([d.name() for d in to_remove]) == sorted(expected_remove)
class NodeAssignmentTest2(NamedTuple):
])
def test_node_assignment2(service_type, placement, hosts,
daemons, expected_len, in_set):
- hosts = HostAssignment(
+ hosts, to_add, to_remove = HostAssignment(
spec=ServiceSpec(service_type, placement=placement),
hosts=[HostSpec(h, labels=['foo']) for h in hosts],
daemons=daemons,
])
def test_node_assignment3(service_type, placement, hosts,
daemons, expected_len, must_have):
- hosts = HostAssignment(
+ hosts, to_add, to_remove = HostAssignment(
spec=ServiceSpec(service_type, placement=placement),
hosts=[HostSpec(h) for h in hosts],
daemons=daemons,
])
def test_bad_specs(service_type, placement, hosts, daemons, expected):
with pytest.raises(OrchestratorValidationError) as e:
- hosts = HostAssignment(
+ hosts, to_add, to_remove = HostAssignment(
spec=ServiceSpec(service_type, placement=placement),
hosts=[HostSpec(h) for h in hosts],
daemons=daemons,
hosts: List[str]
daemons: List[DaemonDescription]
expected: List[List[str]]
+ expected_add: List[List[str]]
+ expected_remove: List[List[str]]
-@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected",
+@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
[
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3'),
],
- [['host1', 'host2'], ['host1', 'host3']]
+ [['host1', 'host2'], ['host1', 'host3']],
+ [[]],
+ [['mgr.b'], ['mgr.c']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3', is_active=True),
],
- [['host1', 'host3'], ['host2', 'host3']]
+ [['host1', 'host3'], ['host2', 'host3']],
+ [[]],
+ [['mgr.a'], ['mgr.b']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2', is_active=True),
DaemonDescription('mgr', 'c', 'host3'),
],
- [['host2']]
+ [['host2']],
+ [[]],
+ [['mgr.a', 'mgr.c']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3', is_active=True),
],
- [['host3']]
+ [['host3']],
+ [[]],
+ [['mgr.a', 'mgr.b']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3', is_active=True),
],
- [['host1'], ['host3']]
+ [['host1'], ['host3']],
+ [[]],
+ [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2', is_active=True),
DaemonDescription('mgr', 'c', 'host3', is_active=True),
],
- [['host2', 'host3']]
+ [['host2', 'host3']],
+ [[]],
+ [['mgr.a']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2', is_active=True),
DaemonDescription('mgr', 'c', 'host3', is_active=True),
],
- [['host1'], ['host2'], ['host3']]
+ [['host1'], ['host2'], ['host3']],
+ [[]],
+ [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3'),
],
- [['host1']]
+ [['host1']],
+ [[]],
+ [['mgr.a2', 'mgr.b', 'mgr.c']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3'),
],
- [['host1']]
+ [['host1']],
+ [[]],
+ [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']]
),
ActiveAssignmentTest(
'mgr',
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3', is_active=True),
],
- [['host1', 'host3']]
+ [['host1', 'host3']],
+ [[]],
+ [['mgr.a2', 'mgr.b']]
),
# Explicit placement should override preference for active daemon
ActiveAssignmentTest(
DaemonDescription('mgr', 'b', 'host2'),
DaemonDescription('mgr', 'c', 'host3', is_active=True),
],
- [['host1']]
+ [['host1']],
+ [[]],
+ [['mgr.b', 'mgr.c']]
),
])
-def test_active_assignment(service_type, placement, hosts, daemons, expected):
+def test_active_assignment(service_type, placement, hosts, daemons, expected, expected_add, expected_remove):
spec = ServiceSpec(service_type=service_type,
service_id=None,
placement=placement)
- hosts = HostAssignment(
+ hosts, to_add, to_remove = HostAssignment(
spec=spec,
hosts=[HostSpec(h) for h in hosts],
daemons=daemons,
).place()
assert sorted([h.hostname for h in hosts]) in expected
+ assert sorted([h.hostname for h in to_add]) in expected_add
+ assert sorted([h.name() for h in to_remove]) in expected_remove