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