Skip to content

Commit 6e67f09

Browse files
mgr/cephadm: Add stick table and haproxy peers in haproxy.cfg for NFS to support nfs active-active cluster
Fixes: https://tracker.ceph.com/issues/72906 Signed-off-by: Shweta Bhosale <[email protected]>
1 parent 7c67feb commit 6e67f09

File tree

6 files changed

+146
-38
lines changed

6 files changed

+146
-38
lines changed

src/pybind/mgr/cephadm/schedule.py

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
)
1616

1717
import orchestrator
18-
from ceph.deployment.service_spec import ServiceSpec
18+
from ceph.deployment.service_spec import ServiceSpec, HostPlacementSpec
1919
from orchestrator._interface import DaemonDescription
2020
from orchestrator import OrchestratorValidationError
2121
from .utils import RESCHEDULE_FROM_OFFLINE_HOSTS_TYPES
@@ -24,6 +24,54 @@
2424
T = TypeVar('T')
2525

2626

27+
def get_placement_hosts(
28+
spec: ServiceSpec,
29+
hosts: List[orchestrator.HostSpec],
30+
draining_hosts: List[orchestrator.HostSpec]
31+
) -> List[HostPlacementSpec]:
32+
"""
33+
Get the list of candidate host placement specs based on placement specifications.
34+
Args:
35+
spec: The service specification
36+
hosts: List of available hosts
37+
draining_hosts: List of hosts that are draining
38+
Returns:
39+
List[HostPlacementSpec]: List of host placement specs that match the placement criteria
40+
"""
41+
if spec.placement.hosts:
42+
host_specs = [
43+
h for h in spec.placement.hosts
44+
if h.hostname not in [dh.hostname for dh in draining_hosts]
45+
]
46+
elif spec.placement.label:
47+
labeled_hosts = [h for h in hosts if spec.placement.label in h.labels]
48+
host_specs = [
49+
HostPlacementSpec(hostname=x.hostname, network='', name='')
50+
for x in labeled_hosts
51+
]
52+
if spec.placement.host_pattern:
53+
matching_hostnames = spec.placement.filter_matching_hostspecs(hosts)
54+
host_specs = [h for h in host_specs if h.hostname in matching_hostnames]
55+
elif spec.placement.host_pattern:
56+
matching_hostnames = spec.placement.filter_matching_hostspecs(hosts)
57+
host_specs = [
58+
HostPlacementSpec(hostname=hostname, network='', name='')
59+
for hostname in matching_hostnames
60+
]
61+
elif (
62+
spec.placement.count is not None
63+
or spec.placement.count_per_host is not None
64+
):
65+
host_specs = [
66+
HostPlacementSpec(hostname=x.hostname, network='', name='')
67+
for x in hosts
68+
]
69+
else:
70+
raise OrchestratorValidationError(
71+
"placement spec is empty: no hosts, no label, no pattern, no count")
72+
return host_specs
73+
74+
2775
class DaemonPlacement(NamedTuple):
2876
daemon_type: str
2977
hostname: str
@@ -453,39 +501,16 @@ def find_ip_on_host(self, hostname: str, subnets: List[str]) -> Optional[str]:
453501
return None
454502

455503
def get_candidates(self) -> List[DaemonPlacement]:
456-
if self.spec.placement.hosts:
457-
ls = [
458-
DaemonPlacement(daemon_type=self.primary_daemon_type,
459-
hostname=h.hostname, network=h.network, name=h.name,
460-
ports=self.ports_start)
461-
for h in self.spec.placement.hosts if h.hostname not in [dh.hostname for dh in self.draining_hosts]
462-
]
463-
elif self.spec.placement.label:
464-
ls = [
465-
DaemonPlacement(daemon_type=self.primary_daemon_type,
466-
hostname=x.hostname, ports=self.ports_start)
467-
for x in self.hosts_by_label(self.spec.placement.label)
468-
]
469-
if self.spec.placement.host_pattern:
470-
ls = [h for h in ls if h.hostname in self.spec.placement.filter_matching_hostspecs(self.hosts)]
471-
elif self.spec.placement.host_pattern:
472-
ls = [
473-
DaemonPlacement(daemon_type=self.primary_daemon_type,
474-
hostname=x, ports=self.ports_start)
475-
for x in self.spec.placement.filter_matching_hostspecs(self.hosts)
476-
]
477-
elif (
478-
self.spec.placement.count is not None
479-
or self.spec.placement.count_per_host is not None
480-
):
481-
ls = [
482-
DaemonPlacement(daemon_type=self.primary_daemon_type,
483-
hostname=x.hostname, ports=self.ports_start)
484-
for x in self.hosts
485-
]
486-
else:
487-
raise OrchestratorValidationError(
488-
"placement spec is empty: no hosts, no label, no pattern, no count")
504+
host_specs = get_placement_hosts(self.spec, self.hosts, self.draining_hosts)
505+
506+
ls = [
507+
DaemonPlacement(daemon_type=self.primary_daemon_type,
508+
hostname=h.hostname,
509+
network=h.network,
510+
name=h.name,
511+
ports=self.ports_start)
512+
for h in host_specs
513+
]
489514

490515
# allocate an IP?
491516
if self.host_selector:

src/pybind/mgr/cephadm/serve.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,14 @@ def _check_daemons(self) -> None:
11611161
only_kmip_updated = all(s.startswith('kmip') for s in list(sym_diff))
11621162
if not only_kmip_updated:
11631163
action = 'redeploy'
1164+
elif dd.daemon_type == 'haproxy':
1165+
if spec and hasattr(spec, 'backend_service'):
1166+
backend_spec = self.mgr.spec_store[spec.backend_service].spec
1167+
if backend_spec.service_type == 'nfs':
1168+
svc = service_registry.get_service('ingress')
1169+
if svc.has_placement_changed(deps, spec):
1170+
self.log.debug(f'Redeploy {spec.service_name()} as placement has changed')
1171+
action = 'redeploy'
11641172
elif spec is not None and hasattr(spec, 'extra_container_args') and dd.extra_container_args != spec.extra_container_args:
11651173
self.log.debug(
11661174
f'{dd.name()} container cli args {dd.extra_container_args} -> {spec.extra_container_args}')

src/pybind/mgr/cephadm/services/cephadmservice.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,9 @@ def ignore_possible_stray(
834834
def get_blocking_daemon_hosts(self, service_name: str) -> List[HostSpec]:
835835
return []
836836

837+
def has_placement_changed(self, deps: List[str], spec: ServiceSpec) -> bool:
838+
return False
839+
837840

838841
class CephService(CephadmService):
839842

src/pybind/mgr/cephadm/services/ingress.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from cephadm.services.cephadmservice import CephadmDaemonDeploySpec, CephService
1212
from .service_registry import register_cephadm_service
1313
from cephadm.tlsobject_types import TLSCredentials
14+
from cephadm.schedule import get_placement_hosts
1415

1516
if TYPE_CHECKING:
1617
from ..module import CephadmOrchestrator
@@ -121,7 +122,10 @@ def get_haproxy_dependencies(mgr: "CephadmOrchestrator", spec: Optional[ServiceS
121122
if ssl_cert_key:
122123
assert isinstance(ssl_cert_key, str)
123124
deps.append(f'ssl-cert-key:{str(utils.md5_hash(ssl_cert_key))}')
124-
125+
backend_spec = mgr.spec_store[ingress_spec.backend_service].spec
126+
if backend_spec.service_type == 'nfs':
127+
hosts = get_placement_hosts(spec, mgr.cache.get_schedulable_hosts(), mgr.cache.get_draining_hosts())
128+
deps.append(f'placement_hosts:{",".join(sorted(h.hostname for h in hosts))}')
125129
return sorted(deps)
126130

127131
def haproxy_generate_config(
@@ -150,6 +154,7 @@ def haproxy_generate_config(
150154
if spec.monitor_password:
151155
password = spec.monitor_password
152156

157+
peer_hosts = {}
153158
if backend_spec.service_type == 'nfs':
154159
mode = 'tcp'
155160
# we need to get the nfs daemon with the highest rank_generation for
@@ -202,6 +207,18 @@ def haproxy_generate_config(
202207
'ip': '0.0.0.0',
203208
'port': 0,
204209
})
210+
# Get peer hosts for haproxy active-active configuration using placement hosts
211+
hosts = get_placement_hosts(
212+
spec,
213+
self.mgr.cache.get_schedulable_hosts(),
214+
self.mgr.cache.get_draining_hosts()
215+
)
216+
if hosts:
217+
for host in hosts:
218+
peer_ip = self.mgr.inventory.get_addr(host.hostname)
219+
peer_hosts[host.hostname] = peer_ip
220+
logger.debug(f"HAProxy peer hosts for {spec.service_name()}: {peer_hosts}")
221+
205222
else:
206223
mode = 'tcp' if spec.use_tcp_mode_over_rgw else 'http'
207224
servers = [
@@ -257,6 +274,7 @@ def haproxy_generate_config(
257274
'health_check_interval': spec.health_check_interval or '2s',
258275
'v4v6_flag': v4v6_flag,
259276
'monitor_ssl_file': monitor_ssl_file,
277+
'peer_hosts': peer_hosts,
260278
}
261279
)
262280
config_files = {
@@ -509,3 +527,24 @@ def get_monitoring_details(self, service_name: str, host: str) -> Tuple[Optional
509527
if not monitor_addr:
510528
logger.debug(f"No IP address found in the network {spec.monitor_networks} on host {host}.")
511529
return monitor_addr, monitor_port
530+
531+
def has_placement_changed(self, deps: List[str], spec: ServiceSpec) -> bool:
532+
"""Check if placement hosts have changed"""
533+
def extract_hosts(deps: List[str]) -> List[str]:
534+
for dep in deps:
535+
if dep.startswith('placement_hosts:'):
536+
host_string = dep.split(':', 1)[1]
537+
return host_string.split(',') if host_string else []
538+
return []
539+
540+
hosts = extract_hosts(deps)
541+
current_hosts = get_placement_hosts(
542+
spec,
543+
self.mgr.cache.get_schedulable_hosts(),
544+
self.mgr.cache.get_draining_hosts()
545+
)
546+
current_hosts = sorted(h.hostname for h in current_hosts)
547+
if current_hosts != hosts:
548+
logger.debug(f'Placement has changed for {spec.service_name()} from {hosts} -> {current_hosts}')
549+
return True
550+
return False

src/pybind/mgr/cephadm/templates/services/ingress/haproxy.cfg.j2

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ frontend frontend
7474
{% endif %}
7575
default_backend backend
7676

77+
{% if backend_spec.service_type == 'nfs' and peer_hosts %}
78+
peers haproxy_peers
79+
{% for hostname, ip in peer_hosts.items() %}
80+
peer {{ hostname }} {{ ip }}:1024
81+
{% endfor %}
82+
83+
{% endif %}
7784
backend backend
7885
{% if mode == 'http' %}
7986
option forwardfor
@@ -87,6 +94,11 @@ backend backend
8794
{% if mode == 'tcp' %}
8895
mode tcp
8996
balance roundrobin
97+
{% if backend_spec.service_type == 'nfs' %}
98+
stick-table type ip size 200k expire 30m peers haproxy_peers
99+
stick on src
100+
{% endif %}
101+
hash-type consistent
90102
{% if spec.use_tcp_mode_over_rgw %}
91103
{% if backend_spec.ssl %}
92104
option ssl-hello-chk

src/pybind/mgr/cephadm/tests/test_services.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,7 +2737,9 @@ def fake_get_addr(hostname: str) -> str:
27372737
monitor_password='12345',
27382738
keepalived_password='12345',
27392739
enable_haproxy_protocol=enable_haproxy_protocol,
2740-
enable_stats=True
2740+
enable_stats=True,
2741+
placement=PlacementSpec(
2742+
hosts=['host1'])
27412743
)
27422744

27432745
cephadm_module.spec_store._specs = {
@@ -2785,9 +2787,14 @@ def fake_get_addr(hostname: str) -> str:
27852787
' bind 192.168.122.100:2049\n'
27862788
' option tcplog\n'
27872789
' default_backend backend\n\n'
2790+
'peers haproxy_peers\n'
2791+
' peer host1 host1:1024\n\n'
27882792
'backend backend\n'
27892793
' mode tcp\n'
27902794
' balance roundrobin\n'
2795+
' stick-table type ip size 200k expire 30m peers haproxy_peers\n'
2796+
' stick on src\n'
2797+
' hash-type consistent\n'
27912798
)
27922799
if enable_haproxy_protocol:
27932800
haproxy_txt += ' default-server send-proxy-v2\n'
@@ -3119,7 +3126,8 @@ def test_haproxy_config_rgw_tcp_mode(self, _run_cephadm, cephadm_module: Cephadm
31193126
monitor_password='12345',
31203127
virtual_interface_networks=['1.2.3.0/24'],
31213128
virtual_ip="1.2.3.4/32",
3122-
use_tcp_mode_over_rgw=True)
3129+
use_tcp_mode_over_rgw=True,
3130+
enable_stats=True)
31233131
with with_service(cephadm_module, s) as _, with_service(cephadm_module, ispec) as _:
31243132
# generate the haproxy conf based on the specified spec
31253133
haproxy_generated_conf = service_registry.get_service('ingress').haproxy_generate_config(
@@ -3637,7 +3645,10 @@ def fake_keys():
36373645
monitor_password='12345',
36383646
keepalived_password='12345',
36393647
enable_haproxy_protocol=True,
3640-
enable_stats=True
3648+
enable_stats=True,
3649+
placement=PlacementSpec(
3650+
count=1,
3651+
hosts=['host1', 'host2']),
36413652
)
36423653

36433654
cephadm_module.spec_store._specs = {
@@ -3681,9 +3692,15 @@ def fake_keys():
36813692
' bind 192.168.122.100:2049\n'
36823693
' option tcplog\n'
36833694
' default_backend backend\n\n'
3695+
'peers haproxy_peers\n'
3696+
' peer host1 192.168.122.111:1024\n'
3697+
' peer host2 192.168.122.222:1024\n\n'
36843698
'backend backend\n'
36853699
' mode tcp\n'
36863700
' balance roundrobin\n'
3701+
' stick-table type ip size 200k expire 30m peers haproxy_peers\n'
3702+
' stick on src\n'
3703+
' hash-type consistent\n'
36873704
' default-server send-proxy-v2\n'
36883705
' server nfs.foo.0 192.168.122.111:12049 check\n'
36893706
)
@@ -3866,6 +3883,10 @@ def test_haproxy_protocol_nfs_config_with_ip_addrs(
38663883
monitor_password='12345',
38673884
keepalived_password='12345',
38683885
enable_haproxy_protocol=True,
3886+
placement=PlacementSpec(
3887+
count=1,
3888+
hosts=['host1', 'host2']),
3889+
38693890
)
38703891
cephadm_module.spec_store._specs = {
38713892
'nfs.foo': nfs_service,

0 commit comments

Comments
 (0)