]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/cephadm/services/nfs.py
import ceph pacific 16.2.5
[ceph.git] / ceph / src / pybind / mgr / cephadm / services / nfs.py
CommitLineData
f67539c2 1import errno
801d1391 2import logging
b3b6e05e
TL
3import os
4import subprocess
5import tempfile
f67539c2
TL
6from typing import Dict, Tuple, Any, List, cast, Optional
7
8from mgr_module import HandleCommandResult
801d1391 9
b3b6e05e 10from ceph.deployment.service_spec import ServiceSpec, NFSServiceSpec
801d1391 11
f67539c2 12from orchestrator import DaemonDescription
801d1391 13
f67539c2 14from cephadm.services.cephadmservice import AuthEntity, CephadmDaemonDeploySpec, CephService
801d1391
TL
15
16logger = logging.getLogger(__name__)
17
e306af50 18
f91f0fd5 19class 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 )