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