]>
Commit | Line | Data |
---|---|---|
f67539c2 | 1 | import errno |
801d1391 | 2 | import logging |
b3b6e05e TL |
3 | import os |
4 | import subprocess | |
5 | import tempfile | |
f67539c2 TL |
6 | from typing import Dict, Tuple, Any, List, cast, Optional |
7 | ||
8 | from mgr_module import HandleCommandResult | |
801d1391 | 9 | |
b3b6e05e | 10 | from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec |
801d1391 | 11 | |
f67539c2 | 12 | from orchestrator import DaemonDescription |
801d1391 | 13 | |
f67539c2 | 14 | from cephadm.services.cephadmservice import AuthEntity, CephadmDaemonDeploySpec, CephService |
801d1391 TL |
15 | |
16 | logger = logging.getLogger(__name__) | |
17 | ||
e306af50 | 18 | |
f91f0fd5 | 19 | class NFSService(CephService): |
f6b5b4d7 TL |
20 | TYPE = 'nfs' |
21 | ||
b3b6e05e TL |
22 | def ranked(self) -> bool: |
23 | return True | |
24 | ||
25 | def fence(self, daemon_id: str) -> None: | |
26 | logger.info(f'Fencing old nfs.{daemon_id}') | |
27 | ret, out, err = self.mgr.mon_command({ | |
28 | 'prefix': 'auth rm', | |
29 | 'entity': f'client.nfs.{daemon_id}', | |
30 | }) | |
31 | ||
32 | # TODO: block/fence this entity (in case it is still running somewhere) | |
33 | ||
34 | def fence_old_ranks(self, | |
35 | spec: ServiceSpec, | |
36 | rank_map: Dict[int, Dict[int, Optional[str]]], | |
37 | num_ranks: int) -> None: | |
38 | for rank, m in list(rank_map.items()): | |
39 | if rank >= num_ranks: | |
40 | for daemon_id in m.values(): | |
41 | if daemon_id is not None: | |
42 | self.fence(daemon_id) | |
43 | del rank_map[rank] | |
44 | nodeid = f'{spec.service_name()}.{rank}' | |
45 | self.mgr.log.info(f'Removing {nodeid} from the ganesha grace table') | |
46 | self.run_grace_tool(cast(NFSServiceSpec, spec), 'remove', nodeid) | |
47 | self.mgr.spec_store.save_rank_map(spec.service_name(), rank_map) | |
48 | else: | |
49 | max_gen = max(m.keys()) | |
50 | for gen, daemon_id in list(m.items()): | |
51 | if gen < max_gen: | |
52 | if daemon_id is not None: | |
53 | self.fence(daemon_id) | |
54 | del rank_map[rank][gen] | |
55 | self.mgr.spec_store.save_rank_map(spec.service_name(), rank_map) | |
56 | ||
f67539c2 | 57 | def config(self, spec: NFSServiceSpec, daemon_id: str) -> None: # type: ignore |
b3b6e05e TL |
58 | from nfs.cluster import create_ganesha_pool |
59 | ||
f6b5b4d7 | 60 | assert self.TYPE == spec.service_type |
adb31ebb | 61 | assert spec.pool |
b3b6e05e | 62 | create_ganesha_pool(self.mgr, spec.pool) |
f6b5b4d7 | 63 | |
f67539c2 | 64 | def prepare_create(self, daemon_spec: CephadmDaemonDeploySpec) -> CephadmDaemonDeploySpec: |
f6b5b4d7 | 65 | assert self.TYPE == daemon_spec.daemon_type |
f67539c2 | 66 | daemon_spec.final_config, daemon_spec.deps = self.generate_config(daemon_spec) |
f91f0fd5 | 67 | return daemon_spec |
801d1391 | 68 | |
f67539c2 | 69 | def generate_config(self, daemon_spec: CephadmDaemonDeploySpec) -> Tuple[Dict[str, Any], List[str]]: |
f91f0fd5 | 70 | assert self.TYPE == daemon_spec.daemon_type |
801d1391 | 71 | |
f91f0fd5 TL |
72 | daemon_type = daemon_spec.daemon_type |
73 | daemon_id = daemon_spec.daemon_id | |
74 | host = daemon_spec.host | |
f67539c2 | 75 | spec = cast(NFSServiceSpec, self.mgr.spec_store[daemon_spec.service_name].spec) |
801d1391 | 76 | |
f91f0fd5 | 77 | deps: List[str] = [] |
801d1391 | 78 | |
b3b6e05e TL |
79 | nodeid = f'{daemon_spec.service_name}.{daemon_spec.rank}' |
80 | ||
f91f0fd5 TL |
81 | # create the RADOS recovery pool keyring |
82 | rados_user = f'{daemon_type}.{daemon_id}' | |
83 | rados_keyring = self.create_keyring(daemon_spec) | |
801d1391 | 84 | |
b3b6e05e TL |
85 | # ensure rank is known to ganesha |
86 | self.mgr.log.info(f'Ensuring {nodeid} is in the ganesha grace table') | |
87 | self.run_grace_tool(spec, 'add', nodeid) | |
88 | ||
f91f0fd5 TL |
89 | # create the rados config object |
90 | self.create_rados_config_obj(spec) | |
91 | ||
92 | # create the RGW keyring | |
93 | rgw_user = f'{rados_user}-rgw' | |
94 | rgw_keyring = self.create_rgw_keyring(daemon_spec) | |
95 | ||
96 | # generate the ganesha config | |
97 | def get_ganesha_conf() -> str: | |
b3b6e05e TL |
98 | context = { |
99 | "user": rados_user, | |
100 | "nodeid": nodeid, | |
101 | "pool": spec.pool, | |
102 | "namespace": spec.namespace if spec.namespace else '', | |
103 | "rgw_user": rgw_user, | |
104 | "url": spec.rados_config_location(), | |
105 | # fall back to default NFS port if not present in daemon_spec | |
106 | "port": daemon_spec.ports[0] if daemon_spec.ports else 2049, | |
107 | "bind_addr": daemon_spec.ip if daemon_spec.ip else '', | |
108 | } | |
f91f0fd5 TL |
109 | return self.mgr.template.render('services/nfs/ganesha.conf.j2', context) |
110 | ||
111 | # generate the cephadm config json | |
112 | def get_cephadm_config() -> Dict[str, Any]: | |
113 | config: Dict[str, Any] = {} | |
114 | config['pool'] = spec.pool | |
115 | if spec.namespace: | |
116 | config['namespace'] = spec.namespace | |
117 | config['userid'] = rados_user | |
118 | config['extra_args'] = ['-N', 'NIV_EVENT'] | |
119 | config['files'] = { | |
120 | 'ganesha.conf': get_ganesha_conf(), | |
121 | } | |
122 | config.update( | |
123 | self.get_config_and_keyring( | |
124 | daemon_type, daemon_id, | |
125 | keyring=rados_keyring, | |
126 | host=host | |
127 | ) | |
128 | ) | |
129 | config['rgw'] = { | |
130 | 'cluster': 'ceph', | |
131 | 'user': rgw_user, | |
132 | 'keyring': rgw_keyring, | |
133 | } | |
134 | logger.debug('Generated cephadm config-json: %s' % config) | |
135 | return config | |
136 | ||
137 | return get_cephadm_config(), deps | |
138 | ||
139 | def create_rados_config_obj(self, | |
140 | spec: NFSServiceSpec, | |
141 | clobber: bool = False) -> None: | |
b3b6e05e TL |
142 | objname = spec.rados_config_name() |
143 | cmd = [ | |
144 | 'rados', | |
145 | '-n', f"mgr.{self.mgr.get_mgr_id()}", | |
146 | '-k', str(self.mgr.get_ceph_option('keyring')), | |
147 | '-p', cast(str, spec.pool), | |
148 | ] | |
149 | if spec.namespace: | |
150 | cmd += ['--namespace', spec.namespace] | |
151 | result = subprocess.run( | |
152 | cmd + ['get', objname, '-'], | |
153 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
154 | timeout=10) | |
155 | if not result.returncode and not clobber: | |
156 | logger.info('Rados config object exists: %s' % objname) | |
157 | else: | |
158 | logger.info('Creating rados config object: %s' % objname) | |
159 | result = subprocess.run( | |
160 | cmd + ['put', objname, '-'], | |
161 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
162 | timeout=10) | |
163 | if result.returncode: | |
164 | self.mgr.log.warning( | |
165 | f'Unable to create rados config object {objname}: {result.stderr.decode("utf-8")}' | |
166 | ) | |
167 | raise RuntimeError(result.stderr.decode("utf-8")) | |
801d1391 | 168 | |
f67539c2 | 169 | def create_keyring(self, daemon_spec: CephadmDaemonDeploySpec) -> str: |
f91f0fd5 | 170 | daemon_id = daemon_spec.daemon_id |
f67539c2 | 171 | spec = cast(NFSServiceSpec, self.mgr.spec_store[daemon_spec.service_name].spec) |
f91f0fd5 TL |
172 | entity: AuthEntity = self.get_auth_entity(daemon_id) |
173 | ||
174 | osd_caps = 'allow rw pool=%s' % (spec.pool) | |
175 | if spec.namespace: | |
176 | osd_caps = '%s namespace=%s' % (osd_caps, spec.namespace) | |
177 | ||
f67539c2 TL |
178 | logger.info('Creating key for %s' % entity) |
179 | keyring = self.get_keyring_with_caps(entity, | |
180 | ['mon', 'allow r', | |
181 | 'osd', osd_caps]) | |
f91f0fd5 TL |
182 | |
183 | return keyring | |
184 | ||
f67539c2 | 185 | def create_rgw_keyring(self, daemon_spec: CephadmDaemonDeploySpec) -> str: |
f91f0fd5 TL |
186 | daemon_id = daemon_spec.daemon_id |
187 | entity: AuthEntity = self.get_auth_entity(f'{daemon_id}-rgw') | |
188 | ||
f67539c2 TL |
189 | logger.info('Creating key for %s' % entity) |
190 | keyring = self.get_keyring_with_caps(entity, | |
191 | ['mon', 'allow r', | |
192 | 'osd', 'allow rwx tag rgw *=*']) | |
f91f0fd5 TL |
193 | |
194 | return keyring | |
195 | ||
b3b6e05e TL |
196 | def run_grace_tool(self, |
197 | spec: NFSServiceSpec, | |
198 | action: str, | |
199 | nodeid: str) -> None: | |
200 | # write a temp keyring and referencing config file. this is a kludge | |
201 | # because the ganesha-grace-tool can only authenticate as a client (and | |
202 | # not a mgr). Also, it doesn't allow you to pass a keyring location via | |
203 | # the command line, nor does it parse the CEPH_ARGS env var. | |
204 | tmp_id = f'mgr.nfs.grace.{spec.service_name()}' | |
205 | entity = AuthEntity(f'client.{tmp_id}') | |
206 | keyring = self.get_keyring_with_caps( | |
207 | entity, | |
208 | ['mon', 'allow r', 'osd', f'allow rwx pool {spec.pool}'] | |
209 | ) | |
210 | tmp_keyring = tempfile.NamedTemporaryFile(mode='w', prefix='mgr-grace-keyring') | |
211 | os.fchmod(tmp_keyring.fileno(), 0o600) | |
212 | tmp_keyring.write(keyring) | |
213 | tmp_keyring.flush() | |
214 | tmp_conf = tempfile.NamedTemporaryFile(mode='w', prefix='mgr-grace-conf') | |
215 | tmp_conf.write(self.mgr.get_minimal_ceph_conf()) | |
216 | tmp_conf.write(f'\tkeyring = {tmp_keyring.name}\n') | |
217 | tmp_conf.flush() | |
218 | try: | |
219 | cmd: List[str] = [ | |
220 | 'ganesha-rados-grace', | |
221 | '--cephconf', tmp_conf.name, | |
222 | '--userid', tmp_id, | |
223 | '--pool', cast(str, spec.pool), | |
224 | ] | |
225 | if spec.namespace: | |
226 | cmd += ['--ns', spec.namespace] | |
227 | cmd += [action, nodeid] | |
228 | self.mgr.log.debug(cmd) | |
229 | result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
230 | timeout=10) | |
231 | if result.returncode: | |
232 | self.mgr.log.warning( | |
233 | f'ganesha-rados-grace tool failed: {result.stderr.decode("utf-8")}' | |
234 | ) | |
235 | raise RuntimeError(f'grace tool failed: {result.stderr.decode("utf-8")}') | |
236 | ||
237 | finally: | |
238 | self.mgr.check_mon_command({ | |
239 | 'prefix': 'auth rm', | |
240 | 'entity': entity, | |
241 | }) | |
242 | ||
f91f0fd5 | 243 | def remove_rgw_keyring(self, daemon: DaemonDescription) -> None: |
f67539c2 | 244 | assert daemon.daemon_id is not None |
f91f0fd5 TL |
245 | daemon_id: str = daemon.daemon_id |
246 | entity: AuthEntity = self.get_auth_entity(f'{daemon_id}-rgw') | |
247 | ||
f67539c2 | 248 | logger.info(f'Removing key for {entity}') |
b3b6e05e | 249 | self.mgr.check_mon_command({ |
f91f0fd5 TL |
250 | 'prefix': 'auth rm', |
251 | 'entity': entity, | |
252 | }) | |
253 | ||
254 | def post_remove(self, daemon: DaemonDescription) -> None: | |
255 | super().post_remove(daemon) | |
256 | self.remove_rgw_keyring(daemon) | |
f67539c2 TL |
257 | |
258 | def ok_to_stop(self, | |
259 | daemon_ids: List[str], | |
260 | force: bool = False, | |
261 | known: Optional[List[str]] = None) -> HandleCommandResult: | |
262 | # if only 1 nfs, alert user (this is not passable with --force) | |
263 | warn, warn_message = self._enough_daemons_to_stop(self.TYPE, daemon_ids, 'NFS', 1, True) | |
264 | if warn: | |
265 | return HandleCommandResult(-errno.EBUSY, '', warn_message) | |
266 | ||
267 | # if reached here, there is > 1 nfs daemon. | |
268 | if force: | |
269 | return HandleCommandResult(0, warn_message, '') | |
270 | ||
271 | # if reached here, > 1 nfs daemon and no force flag. | |
272 | # Provide warning | |
273 | warn_message = "WARNING: Removing NFS daemons can cause clients to lose connectivity. " | |
274 | return HandleCommandResult(-errno.EBUSY, '', warn_message) | |
b3b6e05e TL |
275 | |
276 | def purge(self, service_name: str) -> None: | |
277 | if service_name not in self.mgr.spec_store: | |
278 | return | |
279 | spec = cast(NFSServiceSpec, self.mgr.spec_store[service_name].spec) | |
280 | ||
281 | logger.info(f'Removing grace file for {service_name}') | |
282 | cmd = [ | |
283 | 'rados', | |
284 | '-n', f"mgr.{self.mgr.get_mgr_id()}", | |
285 | '-k', str(self.mgr.get_ceph_option('keyring')), | |
286 | '-p', cast(str, spec.pool), | |
287 | ] | |
288 | if spec.namespace: | |
289 | cmd += ['--namespace', spec.namespace] | |
290 | cmd += ['rm', 'grace'] | |
291 | subprocess.run( | |
292 | cmd, | |
293 | stdout=subprocess.PIPE, | |
294 | stderr=subprocess.PIPE, | |
295 | timeout=10 | |
296 | ) |