]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/nfs/ganesha_conf.py
17d967e77e5cda8049eb86b236e10054d8a2892b
[ceph.git] / ceph / src / pybind / mgr / nfs / ganesha_conf.py
1 from typing import cast, List, Dict, Any, Optional, TYPE_CHECKING
2 from os.path import isabs
3
4 from mgr_module import NFS_GANESHA_SUPPORTED_FSALS
5
6 from .exception import NFSInvalidOperation, FSNotFound
7 from .utils import check_fs
8
9 if TYPE_CHECKING:
10 from nfs.module import Module
11
12
13 def _indentation(depth: int, size: int = 4) -> str:
14 return " " * (depth * size)
15
16
17 def _format_val(block_name: str, key: str, val: str) -> str:
18 if isinstance(val, list):
19 return ', '.join([_format_val(block_name, key, v) for v in val])
20 if isinstance(val, bool):
21 return str(val).lower()
22 if isinstance(val, int) or (block_name == 'CLIENT'
23 and key == 'clients'):
24 return '{}'.format(val)
25 return '"{}"'.format(val)
26
27
28 def _validate_squash(squash: str) -> None:
29 valid_squash_ls = [
30 "root", "root_squash", "rootsquash", "rootid", "root_id_squash",
31 "rootidsquash", "all", "all_squash", "allsquash", "all_anomnymous",
32 "allanonymous", "no_root_squash", "none", "noidsquash",
33 ]
34 if squash.lower() not in valid_squash_ls:
35 raise NFSInvalidOperation(
36 f"squash {squash} not in valid list {valid_squash_ls}"
37 )
38
39
40 def _validate_access_type(access_type: str) -> None:
41 valid_access_types = ['rw', 'ro', 'none']
42 if not isinstance(access_type, str) or access_type.lower() not in valid_access_types:
43 raise NFSInvalidOperation(
44 f'{access_type} is invalid, valid access type are'
45 f'{valid_access_types}'
46 )
47
48
49 class RawBlock():
50 def __init__(self, block_name: str, blocks: List['RawBlock'] = [], values: Dict[str, Any] = {}):
51 if not values: # workaround mutable default argument
52 values = {}
53 if not blocks: # workaround mutable default argument
54 blocks = []
55 self.block_name = block_name
56 self.blocks = blocks
57 self.values = values
58
59 def __eq__(self, other: Any) -> bool:
60 if not isinstance(other, RawBlock):
61 return False
62 return self.block_name == other.block_name and \
63 self.blocks == other.blocks and \
64 self.values == other.values
65
66 def __repr__(self) -> str:
67 return f'RawBlock({self.block_name!r}, {self.blocks!r}, {self.values!r})'
68
69
70 class GaneshaConfParser:
71 def __init__(self, raw_config: str):
72 self.pos = 0
73 self.text = ""
74 for line in raw_config.split("\n"):
75 line = line.lstrip()
76
77 if line.startswith("%"):
78 self.text += line.replace('"', "")
79 self.text += "\n"
80 else:
81 self.text += "".join(line.split())
82
83 def stream(self) -> str:
84 return self.text[self.pos:]
85
86 def last_context(self) -> str:
87 return f'"...{self.text[max(0, self.pos - 30):self.pos]}<here>{self.stream()[:30]}"'
88
89 def parse_block_name(self) -> str:
90 idx = self.stream().find('{')
91 if idx == -1:
92 raise Exception(f"Cannot find block name at {self.last_context()}")
93 block_name = self.stream()[:idx]
94 self.pos += idx + 1
95 return block_name
96
97 def parse_block_or_section(self) -> RawBlock:
98 if self.stream().startswith("%url "):
99 # section line
100 self.pos += 5
101 idx = self.stream().find('\n')
102 if idx == -1:
103 value = self.stream()
104 self.pos += len(value)
105 else:
106 value = self.stream()[:idx]
107 self.pos += idx + 1
108 block_dict = RawBlock('%url', values={'value': value})
109 return block_dict
110
111 block_dict = RawBlock(self.parse_block_name().upper())
112 self.parse_block_body(block_dict)
113 if self.stream()[0] != '}':
114 raise Exception("No closing bracket '}' found at the end of block")
115 self.pos += 1
116 return block_dict
117
118 def parse_parameter_value(self, raw_value: str) -> Any:
119 if raw_value.find(',') != -1:
120 return [self.parse_parameter_value(v.strip())
121 for v in raw_value.split(',')]
122 try:
123 return int(raw_value)
124 except ValueError:
125 if raw_value == "true":
126 return True
127 if raw_value == "false":
128 return False
129 if raw_value.find('"') == 0:
130 return raw_value[1:-1]
131 return raw_value
132
133 def parse_stanza(self, block_dict: RawBlock) -> None:
134 equal_idx = self.stream().find('=')
135 if equal_idx == -1:
136 raise Exception("Malformed stanza: no equal symbol found.")
137 semicolon_idx = self.stream().find(';')
138 parameter_name = self.stream()[:equal_idx].lower()
139 parameter_value = self.stream()[equal_idx + 1:semicolon_idx]
140 block_dict.values[parameter_name] = self.parse_parameter_value(parameter_value)
141 self.pos += semicolon_idx + 1
142
143 def parse_block_body(self, block_dict: RawBlock) -> None:
144 while True:
145 if self.stream().find('}') == 0:
146 # block end
147 return
148
149 last_pos = self.pos
150 semicolon_idx = self.stream().find(';')
151 lbracket_idx = self.stream().find('{')
152 is_semicolon = (semicolon_idx != -1)
153 is_lbracket = (lbracket_idx != -1)
154 is_semicolon_lt_lbracket = (semicolon_idx < lbracket_idx)
155
156 if is_semicolon and ((is_lbracket and is_semicolon_lt_lbracket) or not is_lbracket):
157 self.parse_stanza(block_dict)
158 elif is_lbracket and ((is_semicolon and not is_semicolon_lt_lbracket)
159 or (not is_semicolon)):
160 block_dict.blocks.append(self.parse_block_or_section())
161 else:
162 raise Exception("Malformed stanza: no semicolon found.")
163
164 if last_pos == self.pos:
165 raise Exception("Infinite loop while parsing block content")
166
167 def parse(self) -> List[RawBlock]:
168 blocks = []
169 while self.stream():
170 blocks.append(self.parse_block_or_section())
171 return blocks
172
173
174 class FSAL(object):
175 def __init__(self, name: str) -> None:
176 self.name = name
177
178 @classmethod
179 def from_dict(cls, fsal_dict: Dict[str, Any]) -> 'FSAL':
180 if fsal_dict.get('name') == NFS_GANESHA_SUPPORTED_FSALS[0]:
181 return CephFSFSAL.from_dict(fsal_dict)
182 if fsal_dict.get('name') == NFS_GANESHA_SUPPORTED_FSALS[1]:
183 return RGWFSAL.from_dict(fsal_dict)
184 raise NFSInvalidOperation(f'Unknown FSAL {fsal_dict.get("name")}')
185
186 @classmethod
187 def from_fsal_block(cls, fsal_block: RawBlock) -> 'FSAL':
188 if fsal_block.values.get('name') == NFS_GANESHA_SUPPORTED_FSALS[0]:
189 return CephFSFSAL.from_fsal_block(fsal_block)
190 if fsal_block.values.get('name') == NFS_GANESHA_SUPPORTED_FSALS[1]:
191 return RGWFSAL.from_fsal_block(fsal_block)
192 raise NFSInvalidOperation(f'Unknown FSAL {fsal_block.values.get("name")}')
193
194 def to_fsal_block(self) -> RawBlock:
195 raise NotImplementedError
196
197 def to_dict(self) -> Dict[str, Any]:
198 raise NotImplementedError
199
200
201 class CephFSFSAL(FSAL):
202 def __init__(self,
203 name: str,
204 user_id: Optional[str] = None,
205 fs_name: Optional[str] = None,
206 sec_label_xattr: Optional[str] = None,
207 cephx_key: Optional[str] = None) -> None:
208 super().__init__(name)
209 assert name == 'CEPH'
210 self.fs_name = fs_name
211 self.user_id = user_id
212 self.sec_label_xattr = sec_label_xattr
213 self.cephx_key = cephx_key
214
215 @classmethod
216 def from_fsal_block(cls, fsal_block: RawBlock) -> 'CephFSFSAL':
217 return cls(fsal_block.values['name'],
218 fsal_block.values.get('user_id'),
219 fsal_block.values.get('filesystem'),
220 fsal_block.values.get('sec_label_xattr'),
221 fsal_block.values.get('secret_access_key'))
222
223 def to_fsal_block(self) -> RawBlock:
224 result = RawBlock('FSAL', values={'name': self.name})
225
226 if self.user_id:
227 result.values['user_id'] = self.user_id
228 if self.fs_name:
229 result.values['filesystem'] = self.fs_name
230 if self.sec_label_xattr:
231 result.values['sec_label_xattr'] = self.sec_label_xattr
232 if self.cephx_key:
233 result.values['secret_access_key'] = self.cephx_key
234 return result
235
236 @classmethod
237 def from_dict(cls, fsal_dict: Dict[str, Any]) -> 'CephFSFSAL':
238 return cls(fsal_dict['name'],
239 fsal_dict.get('user_id'),
240 fsal_dict.get('fs_name'),
241 fsal_dict.get('sec_label_xattr'),
242 fsal_dict.get('cephx_key'))
243
244 def to_dict(self) -> Dict[str, str]:
245 r = {'name': self.name}
246 if self.user_id:
247 r['user_id'] = self.user_id
248 if self.fs_name:
249 r['fs_name'] = self.fs_name
250 if self.sec_label_xattr:
251 r['sec_label_xattr'] = self.sec_label_xattr
252 return r
253
254
255 class RGWFSAL(FSAL):
256 def __init__(self,
257 name: str,
258 user_id: Optional[str] = None,
259 access_key_id: Optional[str] = None,
260 secret_access_key: Optional[str] = None
261 ) -> None:
262 super().__init__(name)
263 assert name == 'RGW'
264 # RGW user uid
265 self.user_id = user_id
266 # S3 credentials
267 self.access_key_id = access_key_id
268 self.secret_access_key = secret_access_key
269
270 @classmethod
271 def from_fsal_block(cls, fsal_block: RawBlock) -> 'RGWFSAL':
272 return cls(fsal_block.values['name'],
273 fsal_block.values.get('user_id'),
274 fsal_block.values.get('access_key_id'),
275 fsal_block.values.get('secret_access_key'))
276
277 def to_fsal_block(self) -> RawBlock:
278 result = RawBlock('FSAL', values={'name': self.name})
279
280 if self.user_id:
281 result.values['user_id'] = self.user_id
282 if self.access_key_id:
283 result.values['access_key_id'] = self.access_key_id
284 if self.secret_access_key:
285 result.values['secret_access_key'] = self.secret_access_key
286 return result
287
288 @classmethod
289 def from_dict(cls, fsal_dict: Dict[str, str]) -> 'RGWFSAL':
290 return cls(fsal_dict['name'],
291 fsal_dict.get('user_id'),
292 fsal_dict.get('access_key_id'),
293 fsal_dict.get('secret_access_key'))
294
295 def to_dict(self) -> Dict[str, str]:
296 r = {'name': self.name}
297 if self.user_id:
298 r['user_id'] = self.user_id
299 if self.access_key_id:
300 r['access_key_id'] = self.access_key_id
301 if self.secret_access_key:
302 r['secret_access_key'] = self.secret_access_key
303 return r
304
305
306 class Client:
307 def __init__(self,
308 addresses: List[str],
309 access_type: str,
310 squash: str):
311 self.addresses = addresses
312 self.access_type = access_type
313 self.squash = squash
314
315 @classmethod
316 def from_client_block(cls, client_block: RawBlock) -> 'Client':
317 addresses = client_block.values.get('clients', [])
318 if isinstance(addresses, str):
319 addresses = [addresses]
320 return cls(addresses,
321 client_block.values.get('access_type', None),
322 client_block.values.get('squash', None))
323
324 def to_client_block(self) -> RawBlock:
325 result = RawBlock('CLIENT', values={'clients': self.addresses})
326 if self.access_type:
327 result.values['access_type'] = self.access_type
328 if self.squash:
329 result.values['squash'] = self.squash
330 return result
331
332 @classmethod
333 def from_dict(cls, client_dict: Dict[str, Any]) -> 'Client':
334 return cls(client_dict['addresses'], client_dict['access_type'],
335 client_dict['squash'])
336
337 def to_dict(self) -> Dict[str, Any]:
338 return {
339 'addresses': self.addresses,
340 'access_type': self.access_type,
341 'squash': self.squash
342 }
343
344
345 class Export:
346 def __init__(
347 self,
348 export_id: int,
349 path: str,
350 cluster_id: str,
351 pseudo: str,
352 access_type: str,
353 squash: str,
354 security_label: bool,
355 protocols: List[int],
356 transports: List[str],
357 fsal: FSAL,
358 clients: Optional[List[Client]] = None) -> None:
359 self.export_id = export_id
360 self.path = path
361 self.fsal = fsal
362 self.cluster_id = cluster_id
363 self.pseudo = pseudo
364 self.access_type = access_type
365 self.squash = squash
366 self.attr_expiration_time = 0
367 self.security_label = security_label
368 self.protocols = protocols
369 self.transports = transports
370 self.clients: List[Client] = clients or []
371
372 @classmethod
373 def from_export_block(cls, export_block: RawBlock, cluster_id: str) -> 'Export':
374 fsal_blocks = [b for b in export_block.blocks
375 if b.block_name == "FSAL"]
376
377 client_blocks = [b for b in export_block.blocks
378 if b.block_name == "CLIENT"]
379
380 protocols = export_block.values.get('protocols')
381 if not isinstance(protocols, list):
382 protocols = [protocols]
383
384 transports = export_block.values.get('transports')
385 if isinstance(transports, str):
386 transports = [transports]
387 elif not transports:
388 transports = []
389
390 return cls(export_block.values['export_id'],
391 export_block.values['path'],
392 cluster_id,
393 export_block.values['pseudo'],
394 export_block.values.get('access_type', 'none'),
395 export_block.values.get('squash', 'no_root_squash'),
396 export_block.values.get('security_label', True),
397 protocols,
398 transports,
399 FSAL.from_fsal_block(fsal_blocks[0]),
400 [Client.from_client_block(client)
401 for client in client_blocks])
402
403 def to_export_block(self) -> RawBlock:
404 result = RawBlock('EXPORT', values={
405 'export_id': self.export_id,
406 'path': self.path,
407 'pseudo': self.pseudo,
408 'access_type': self.access_type,
409 'squash': self.squash,
410 'attr_expiration_time': self.attr_expiration_time,
411 'security_label': self.security_label,
412 'protocols': self.protocols,
413 'transports': self.transports,
414 })
415 result.blocks = [
416 self.fsal.to_fsal_block()
417 ] + [
418 client.to_client_block()
419 for client in self.clients
420 ]
421 return result
422
423 @classmethod
424 def from_dict(cls, export_id: int, ex_dict: Dict[str, Any]) -> 'Export':
425 return cls(export_id,
426 ex_dict.get('path', '/'),
427 ex_dict['cluster_id'],
428 ex_dict['pseudo'],
429 ex_dict.get('access_type', 'RO'),
430 ex_dict.get('squash', 'no_root_squash'),
431 ex_dict.get('security_label', True),
432 ex_dict.get('protocols', [4]),
433 ex_dict.get('transports', ['TCP']),
434 FSAL.from_dict(ex_dict.get('fsal', {})),
435 [Client.from_dict(client) for client in ex_dict.get('clients', [])])
436
437 def to_dict(self) -> Dict[str, Any]:
438 return {
439 'export_id': self.export_id,
440 'path': self.path,
441 'cluster_id': self.cluster_id,
442 'pseudo': self.pseudo,
443 'access_type': self.access_type,
444 'squash': self.squash,
445 'security_label': self.security_label,
446 'protocols': sorted([p for p in self.protocols]),
447 'transports': sorted([t for t in self.transports]),
448 'fsal': self.fsal.to_dict(),
449 'clients': [client.to_dict() for client in self.clients]
450 }
451
452 def validate(self, mgr: 'Module') -> None:
453 if not isabs(self.pseudo) or self.pseudo == "/":
454 raise NFSInvalidOperation(
455 f"pseudo path {self.pseudo} is invalid. It should be an absolute "
456 "path and it cannot be just '/'."
457 )
458
459 _validate_squash(self.squash)
460 _validate_access_type(self.access_type)
461
462 if not isinstance(self.security_label, bool):
463 raise NFSInvalidOperation('security_label must be a boolean value')
464
465 for p in self.protocols:
466 if p not in [3, 4]:
467 raise NFSInvalidOperation(f"Invalid protocol {p}")
468
469 valid_transport = ["UDP", "TCP"]
470 for trans in self.transports:
471 if trans.upper() not in valid_transport:
472 raise NFSInvalidOperation(f'{trans} is not a valid transport protocol')
473
474 for client in self.clients:
475 if client.squash:
476 _validate_squash(client.squash)
477 if client.access_type:
478 _validate_access_type(client.access_type)
479
480 if self.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[0]:
481 fs = cast(CephFSFSAL, self.fsal)
482 if not fs.fs_name or not check_fs(mgr, fs.fs_name):
483 raise FSNotFound(fs.fs_name)
484 elif self.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[1]:
485 rgw = cast(RGWFSAL, self.fsal) # noqa
486 pass
487 else:
488 raise NFSInvalidOperation('FSAL {self.fsal.name} not supported')
489
490 def __eq__(self, other: Any) -> bool:
491 if not isinstance(other, Export):
492 return False
493 return self.to_dict() == other.to_dict()
494
495
496 def _format_block_body(block: RawBlock, depth: int = 0) -> str:
497 conf_str = ""
498 for blo in block.blocks:
499 conf_str += format_block(blo, depth)
500
501 for key, val in block.values.items():
502 if val is not None:
503 conf_str += _indentation(depth)
504 fval = _format_val(block.block_name, key, val)
505 conf_str += '{} = {};\n'.format(key, fval)
506 return conf_str
507
508
509 def format_block(block: RawBlock, depth: int = 0) -> str:
510 """Format a raw block object into text suitable as a ganesha configuration
511 block.
512 """
513 if block.block_name == "%url":
514 return '%url "{}"\n\n'.format(block.values['value'])
515
516 conf_str = ""
517 conf_str += _indentation(depth)
518 conf_str += format(block.block_name)
519 conf_str += " {\n"
520 conf_str += _format_block_body(block, depth + 1)
521 conf_str += _indentation(depth)
522 conf_str += "}\n"
523 return conf_str