Skip to content

Commit a5f0773

Browse files
authored
Merge pull request ceph#65531 from ShwetaBhosale1/fix_issue_72906_haproxy_changes_for_nfs_active_active_support
mgr/cephadm: Add stick table and haproxy peers in haproxy.cfg for NFS to support nfs active-active cluster Reviewed-by: Adam King <[email protected]>
2 parents 64fde3c + 6e67f09 commit a5f0773

File tree

7 files changed

+240
-44
lines changed

7 files changed

+240
-44
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: 41 additions & 2 deletions
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,8 +207,20 @@ 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:
206-
mode = 'http'
223+
mode = 'tcp' if spec.use_tcp_mode_over_rgw else 'http'
207224
servers = [
208225
{
209226
'name': d.name(),
@@ -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: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,19 @@ frontend frontend
6868
bind {{ ip }}:{{ frontend_port }} ssl crt /var/lib/haproxy/haproxy.pem {{ v4v6_flag }}
6969
{% else %}
7070
bind {{ ip }}:{{ frontend_port }} {{ v4v6_flag }}
71+
{% endif %}
72+
{% if mode == 'tcp' %}
73+
option tcplog
7174
{% endif %}
7275
default_backend backend
7376

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 %}
7484
backend backend
7585
{% if mode == 'http' %}
7686
option forwardfor
@@ -80,17 +90,29 @@ backend backend
8090
{% endif %}
8191
balance static-rr
8292
option httpchk HEAD / HTTP/1.0
83-
{% for server in servers %}
84-
server {{ server.name }} {{ server.ip }}:{{ server.port }} check weight 100 inter {{ health_check_interval }}
85-
{% endfor %}
8693
{% endif %}
8794
{% if mode == 'tcp' %}
8895
mode tcp
89-
balance source
96+
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 %}
90101
hash-type consistent
102+
{% if spec.use_tcp_mode_over_rgw %}
103+
{% if backend_spec.ssl %}
104+
option ssl-hello-chk
105+
{% endif %}
106+
{% endif %}
91107
{% if default_server_opts %}
92108
default-server {{ default_server_opts|join(" ") }}
93109
{% endif %}
110+
{% endif %}
111+
{% if backend_spec.service_type == 'rgw' %}
112+
{% for server in servers %}
113+
server {{ server.name }} {{ server.ip }}:{{ server.port }} check weight 100 inter {{ health_check_interval }}
114+
{% endfor %}
115+
{% else %}
94116
{% for server in servers %}
95117
server {{ server.name }} {{ server.ip }}:{{ server.port }} check
96118
{% endfor %}

0 commit comments

Comments
 (0)