]>
Commit | Line | Data |
---|---|---|
f67539c2 TL |
1 | import ipaddress |
2 | import logging | |
3 | import random | |
4 | import string | |
5 | from typing import List, Dict, Any, Tuple, cast, Optional | |
6 | ||
7 | from ceph.deployment.service_spec import IngressSpec | |
8 | from cephadm.utils import resolve_ip | |
9 | ||
10 | from cephadm.services.cephadmservice import CephadmDaemonDeploySpec, CephService | |
11 | ||
12 | logger = logging.getLogger(__name__) | |
13 | ||
14 | ||
15 | class IngressService(CephService): | |
16 | TYPE = 'ingress' | |
17 | ||
18 | def primary_daemon_type(self) -> str: | |
19 | return 'haproxy' | |
20 | ||
21 | def per_host_daemon_type(self) -> Optional[str]: | |
22 | return 'keepalived' | |
23 | ||
24 | def prepare_create( | |
25 | self, | |
26 | daemon_spec: CephadmDaemonDeploySpec, | |
27 | ) -> CephadmDaemonDeploySpec: | |
28 | if daemon_spec.daemon_type == 'haproxy': | |
29 | return self.haproxy_prepare_create(daemon_spec) | |
30 | if daemon_spec.daemon_type == 'keepalived': | |
31 | return self.keepalived_prepare_create(daemon_spec) | |
32 | assert False, "unexpected daemon type" | |
33 | ||
34 | def generate_config( | |
35 | self, | |
36 | daemon_spec: CephadmDaemonDeploySpec | |
37 | ) -> Tuple[Dict[str, Any], List[str]]: | |
38 | if daemon_spec.daemon_type == 'haproxy': | |
39 | return self.haproxy_generate_config(daemon_spec) | |
40 | else: | |
41 | return self.keepalived_generate_config(daemon_spec) | |
42 | assert False, "unexpected daemon type" | |
43 | ||
44 | def haproxy_prepare_create( | |
45 | self, | |
46 | daemon_spec: CephadmDaemonDeploySpec, | |
47 | ) -> CephadmDaemonDeploySpec: | |
48 | assert daemon_spec.daemon_type == 'haproxy' | |
49 | ||
50 | daemon_id = daemon_spec.daemon_id | |
51 | host = daemon_spec.host | |
52 | spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec) | |
53 | ||
54 | logger.debug('prepare_create haproxy.%s on host %s with spec %s' % ( | |
55 | daemon_id, host, spec)) | |
56 | ||
57 | daemon_spec.final_config, daemon_spec.deps = self.haproxy_generate_config(daemon_spec) | |
58 | ||
59 | return daemon_spec | |
60 | ||
61 | def haproxy_generate_config( | |
62 | self, | |
63 | daemon_spec: CephadmDaemonDeploySpec, | |
64 | ) -> Tuple[Dict[str, Any], List[str]]: | |
65 | spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec) | |
66 | assert spec.backend_service | |
67 | daemons = self.mgr.cache.get_daemons_by_service(spec.backend_service) | |
68 | deps = [d.name() for d in daemons] | |
69 | ||
70 | # generate password? | |
71 | pw_key = f'{spec.service_name()}/monitor_password' | |
72 | password = self.mgr.get_store(pw_key) | |
73 | if password is None: | |
74 | if not spec.monitor_password: | |
75 | password = ''.join(random.choice(string.ascii_lowercase) for _ in range(20)) | |
76 | self.mgr.set_store(pw_key, password) | |
77 | else: | |
78 | if spec.monitor_password: | |
79 | self.mgr.set_store(pw_key, None) | |
80 | if spec.monitor_password: | |
81 | password = spec.monitor_password | |
82 | ||
83 | haproxy_conf = self.mgr.template.render( | |
84 | 'services/ingress/haproxy.cfg.j2', | |
85 | { | |
86 | 'spec': spec, | |
87 | 'servers': [ | |
88 | { | |
89 | 'name': d.name(), | |
90 | 'ip': d.ip or resolve_ip(str(d.hostname)), | |
91 | 'port': d.ports[0], | |
92 | } for d in daemons if d.ports | |
93 | ], | |
94 | 'user': spec.monitor_user or 'admin', | |
95 | 'password': password, | |
96 | 'ip': daemon_spec.ip or '*', | |
97 | 'frontend_port': daemon_spec.ports[0] if daemon_spec.ports else spec.frontend_port, | |
98 | 'monitor_port': daemon_spec.ports[1] if daemon_spec.ports else spec.monitor_port, | |
99 | } | |
100 | ) | |
101 | config_files = { | |
102 | 'files': { | |
103 | "haproxy.cfg": haproxy_conf, | |
104 | } | |
105 | } | |
106 | if spec.ssl_cert: | |
107 | ssl_cert = spec.ssl_cert | |
108 | if isinstance(ssl_cert, list): | |
109 | ssl_cert = '\n'.join(ssl_cert) | |
110 | config_files['files']['haproxy.pem'] = ssl_cert | |
111 | ||
112 | return config_files, sorted(deps) | |
113 | ||
114 | def keepalived_prepare_create( | |
115 | self, | |
116 | daemon_spec: CephadmDaemonDeploySpec, | |
117 | ) -> CephadmDaemonDeploySpec: | |
118 | assert daemon_spec.daemon_type == 'keepalived' | |
119 | ||
120 | daemon_id = daemon_spec.daemon_id | |
121 | host = daemon_spec.host | |
122 | spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec) | |
123 | ||
124 | logger.debug('prepare_create keepalived.%s on host %s with spec %s' % ( | |
125 | daemon_id, host, spec)) | |
126 | ||
127 | daemon_spec.final_config, daemon_spec.deps = self.keepalived_generate_config(daemon_spec) | |
128 | ||
129 | return daemon_spec | |
130 | ||
131 | def keepalived_generate_config( | |
132 | self, | |
133 | daemon_spec: CephadmDaemonDeploySpec, | |
134 | ) -> Tuple[Dict[str, Any], List[str]]: | |
135 | spec = cast(IngressSpec, self.mgr.spec_store[daemon_spec.service_name].spec) | |
136 | assert spec.backend_service | |
137 | ||
138 | # generate password? | |
139 | pw_key = f'{spec.service_name()}/keepalived_password' | |
140 | password = self.mgr.get_store(pw_key) | |
141 | if password is None: | |
142 | if not spec.keepalived_password: | |
143 | password = ''.join(random.choice(string.ascii_lowercase) for _ in range(20)) | |
144 | self.mgr.set_store(pw_key, password) | |
145 | else: | |
146 | if spec.keepalived_password: | |
147 | self.mgr.set_store(pw_key, None) | |
148 | if spec.keepalived_password: | |
149 | password = spec.keepalived_password | |
150 | ||
151 | daemons = self.mgr.cache.get_daemons_by_service(spec.service_name()) | |
152 | deps = sorted([d.name() for d in daemons if d.daemon_type == 'haproxy']) | |
153 | ||
154 | host = daemon_spec.host | |
155 | hosts = sorted(list(set([str(d.hostname) for d in daemons]))) | |
156 | ||
157 | # interface | |
158 | bare_ip = str(spec.virtual_ip).split('/')[0] | |
159 | interface = None | |
160 | for subnet, ifaces in self.mgr.cache.networks.get(host, {}).items(): | |
161 | if ifaces and ipaddress.ip_address(bare_ip) in ipaddress.ip_network(subnet): | |
162 | interface = list(ifaces.keys())[0] | |
163 | logger.info( | |
164 | f'{bare_ip} is in {subnet} on {host} interface {interface}' | |
165 | ) | |
166 | break | |
167 | if not interface and spec.networks: | |
168 | # hmm, try spec.networks | |
169 | for subnet, ifaces in self.mgr.cache.networks.get(host, {}).items(): | |
170 | if subnet in spec.networks: | |
171 | interface = list(ifaces.keys())[0] | |
172 | logger.info( | |
173 | f'{spec.virtual_ip} will be configured on {host} interface ' | |
174 | f'{interface} (which has guiding subnet {subnet})' | |
175 | ) | |
176 | break | |
177 | if not interface: | |
178 | interface = 'eth0' | |
179 | ||
180 | # script to monitor health | |
181 | script = '/usr/bin/false' | |
182 | for d in daemons: | |
183 | if d.hostname == host: | |
184 | if d.daemon_type == 'haproxy': | |
185 | assert d.ports | |
186 | port = d.ports[1] # monitoring port | |
187 | script = f'/usr/bin/curl http://{d.ip or "localhost"}:{port}/health' | |
188 | assert script | |
189 | ||
190 | # set state. first host in placement is master all others backups | |
191 | state = 'BACKUP' | |
192 | if hosts[0] == host: | |
193 | state = 'MASTER' | |
194 | ||
195 | # remove host, daemon is being deployed on from hosts list for | |
196 | # other_ips in conf file and converter to ips | |
197 | hosts.remove(host) | |
198 | other_ips = [resolve_ip(h) for h in hosts] | |
199 | ||
200 | keepalived_conf = self.mgr.template.render( | |
201 | 'services/ingress/keepalived.conf.j2', | |
202 | { | |
203 | 'spec': spec, | |
204 | 'script': script, | |
205 | 'password': password, | |
206 | 'interface': interface, | |
207 | 'state': state, | |
208 | 'other_ips': other_ips, | |
209 | 'host_ip': resolve_ip(host), | |
210 | } | |
211 | ) | |
212 | ||
213 | config_file = { | |
214 | 'files': { | |
215 | "keepalived.conf": keepalived_conf, | |
216 | } | |
217 | } | |
218 | ||
219 | return config_file, deps |