]>
Commit | Line | Data |
---|---|---|
a4b75251 TL |
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 | class 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 | 34 | class 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 |
181 | class 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 | ||
208 | class 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 | ||
262 | class 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 | ||
313 | class 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 | ||
352 | class 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() |