1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-lines
3 from __future__
import absolute_import
8 from typing
import Any
, Dict
, List
, Optional
, cast
10 from ceph
.deployment
.service_spec
import NFSServiceSpec
11 from orchestrator
import DaemonDescription
, OrchestratorError
, ServiceDescription
14 from ..exceptions
import DashboardException
15 from ..settings
import Settings
16 from .cephfs
import CephFS
17 from .cephx
import CephX
18 from .orchestrator
import OrchClient
19 from .rgw_client
import RgwClient
, RequestException
, NoCredentialsException
22 logger
= logging
.getLogger('ganesha')
25 class NFSException(DashboardException
):
26 def __init__(self
, msg
):
27 super(NFSException
, self
).__init
__(component
="nfs", msg
=msg
)
30 class Ganesha(object):
32 def _get_clusters_locations(cls
):
33 # pylint: disable=too-many-branches
34 # Get Orchestrator clusters
35 orch_result
= cls
._get
_orch
_clusters
_locations
()
37 # Get user-defined clusters
38 location_list_str
= Settings
.GANESHA_CLUSTERS_RADOS_POOL_NAMESPACE
39 if not orch_result
and not location_list_str
:
40 raise NFSException("NFS-Ganesha cluster is not detected. "
41 "Please set the GANESHA_RADOS_POOL_NAMESPACE "
42 "setting or deploy an NFS-Ganesha cluster with the Orchestrator.")
43 result
= {} # type: ignore
44 location_list
= [loc
.strip() for loc
in location_list_str
.split(
45 ",")] if location_list_str
else []
46 for location
in location_list
:
51 raise NFSException("Invalid Ganesha cluster RADOS "
52 "[cluster_id:]pool/namespace setting: {}"
54 if location
.count(':') < 1:
56 if location
.count('/') > 1:
57 raise NFSException("Invalid Ganesha RADOS pool/namespace "
58 "setting: {}".format(location
))
59 # in this case accept pool/namespace only
61 if location
.count('/') == 0:
62 pool
, namespace
= location
, None
64 pool
, namespace
= location
.split('/', 1)
66 cluster
= location
[:location
.find(':')]
67 pool_nm
= location
[location
.find(':')+1:]
68 if pool_nm
.count('/') == 0:
69 pool
, namespace
= pool_nm
, None
71 pool
, namespace
= pool_nm
.split('/', 1)
73 if cluster
in orch_result
:
74 # cephadm might have set same cluster settings, ask the user to remove it.
76 'Detected a conflicting NFS-Ganesha cluster name `{0}`. There exists an '
77 'NFS-Ganesha cluster called `{0}` that is deployed by the Orchestrator. '
78 'Please remove or rename the cluster from the GANESHA_RADOS_POOL_NAMESPACE '
79 'setting.'.format(cluster
))
82 raise NFSException("Duplicate Ganesha cluster definition in "
83 "the setting: {}".format(location_list_str
))
86 'namespace': namespace
,
87 'type': ClusterType
.USER
,
90 return {**orch_result
, **result
}
93 def _get_orch_clusters_locations(cls
):
94 orch_result
= {} # type: ignore
95 services
= cls
._get
_orch
_nfs
_services
()
96 for service
in services
:
97 spec
= cast(NFSServiceSpec
, service
.spec
)
99 orch_result
[spec
.service_id
] = {
101 'namespace': spec
.namespace
,
102 'type': ClusterType
.ORCHESTRATOR
,
103 'daemon_conf': spec
.rados_config_name()
105 except AttributeError as ex
:
106 logger
.warning('Error when getting NFS service from the Orchestrator. %s', str(ex
))
111 def get_ganesha_clusters(cls
):
112 return [cluster_id
for cluster_id
in cls
._get
_clusters
_locations
()]
115 def _get_orch_nfs_services() -> List
[ServiceDescription
]:
117 return OrchClient
.instance().services
.list('nfs')
118 except (RuntimeError, OrchestratorError
, ImportError):
122 def parse_rados_url(cls
, rados_url
):
123 if not rados_url
.startswith("rados://"):
124 raise NFSException("Invalid NFS Ganesha RADOS configuration URL: {}"
126 rados_url
= rados_url
[8:]
127 url_comps
= rados_url
.split("/")
128 if len(url_comps
) < 2 or len(url_comps
) > 3:
129 raise NFSException("Invalid NFS Ganesha RADOS configuration URL: "
130 "rados://{}".format(rados_url
))
131 if len(url_comps
) == 2:
132 return url_comps
[0], None, url_comps
[1]
136 def make_rados_url(cls
, pool
, namespace
, obj
):
138 return "rados://{}/{}/{}".format(pool
, namespace
, obj
)
139 return "rados://{}/{}".format(pool
, obj
)
142 def get_cluster(cls
, cluster_id
):
143 locations
= cls
._get
_clusters
_locations
()
144 if cluster_id
not in locations
:
145 raise NFSException("Cluster not found: cluster_id={}"
147 return locations
[cluster_id
]
150 def fsals_available(cls
):
152 if CephFS
.list_filesystems():
153 result
.append("CEPH")
155 if RgwClient
.admin_instance().is_service_online() and \
156 RgwClient
.admin_instance().is_system_user():
158 except (NoCredentialsException
, RequestException
, LookupError):
163 class GaneshaConfParser(object):
164 def __init__(self
, raw_config
):
167 self
.clean_config(raw_config
)
169 def clean_config(self
, raw_config
):
170 for line
in raw_config
.split("\n"):
171 cardinal_idx
= line
.find('#')
172 if cardinal_idx
== -1:
176 self
.text
+= line
[:cardinal_idx
]
177 if line
.startswith("%"):
180 def remove_all_whitespaces(self
):
184 for i
, cha
in enumerate(self
.text
):
186 if cha
!= '"' and self
.text
[i
-1] != '\\':
191 elif i
== (len(self
.text
)-1):
192 if cha
!= '"' and self
.text
[i
-1] != '\\':
195 elif not in_section
and (i
== 0 or self
.text
[i
-1] == '\n') and cha
== '%':
198 elif in_string
or cha
not in [' ', '\n', '\t']:
200 elif cha
== '"' and self
.text
[i
-1] != '\\':
201 in_string
= not in_string
205 return self
.text
[self
.pos
:]
207 def parse_block_name(self
):
208 idx
= self
.stream().find('{')
210 raise Exception("Cannot find block name")
211 block_name
= self
.stream()[:idx
]
215 def parse_block_or_section(self
):
216 if self
.stream().startswith("%url "):
219 idx
= self
.stream().find('\n')
221 value
= self
.stream()
222 self
.pos
+= len(self
.stream())
224 value
= self
.stream()[:idx
]
226 block_dict
= {'block_name': '%url', 'value': value
}
229 block_name
= self
.parse_block_name().upper()
230 block_dict
= {'block_name': block_name
}
231 self
.parse_block_body(block_dict
)
232 if self
.stream()[0] != '}':
233 raise Exception("No closing bracket '}' found at the end of block")
237 def parse_parameter_value(self
, raw_value
):
238 colon_idx
= raw_value
.find(',')
242 return int(raw_value
)
244 if raw_value
== "true":
246 if raw_value
== "false":
248 if raw_value
.find('"') == 0:
249 return raw_value
[1:-1]
252 return [self
.parse_parameter_value(v
.strip())
253 for v
in raw_value
.split(',')]
255 def parse_stanza(self
, block_dict
):
256 equal_idx
= self
.stream().find('=')
257 semicolon_idx
= self
.stream().find(';')
259 raise Exception("Malformed stanza: no equal symbol found.")
260 parameter_name
= self
.stream()[:equal_idx
].lower()
261 parameter_value
= self
.stream()[equal_idx
+1:semicolon_idx
]
262 block_dict
[parameter_name
] = self
.parse_parameter_value(
264 self
.pos
+= semicolon_idx
+1
266 def parse_block_body(self
, block_dict
):
269 semicolon_idx
= self
.stream().find(';')
270 lbracket_idx
= self
.stream().find('{')
271 rbracket_idx
= self
.stream().find('}')
273 if rbracket_idx
== 0:
277 if (semicolon_idx
!= -1 and lbracket_idx
!= -1
278 and semicolon_idx
< lbracket_idx
) \
279 or (semicolon_idx
!= -1 and lbracket_idx
== -1):
280 self
.parse_stanza(block_dict
)
281 elif (semicolon_idx
!= -1 and lbracket_idx
!= -1
282 and semicolon_idx
> lbracket_idx
) or (
283 semicolon_idx
== -1 and lbracket_idx
!= -1):
284 if '_blocks_' not in block_dict
:
285 block_dict
['_blocks_'] = []
286 block_dict
['_blocks_'].append(self
.parse_block_or_section())
288 raise Exception("Malformed stanza: no semicolon found.")
290 if last_pos
== self
.pos
:
291 raise Exception("Infinite loop while parsing block content")
295 self
.remove_all_whitespaces()
298 block_dict
= self
.parse_block_or_section()
299 blocks
.append(block_dict
)
303 def _indentation(depth
, size
=4):
305 for _
in range(0, depth
*size
):
310 def write_block_body(block
, depth
=0):
311 def format_val(key
, val
):
312 if isinstance(val
, list):
313 return ', '.join([format_val(key
, v
) for v
in val
])
314 if isinstance(val
, bool):
315 return str(val
).lower()
316 if isinstance(val
, int) or (block
['block_name'] == 'CLIENT'
317 and key
== 'clients'):
318 return '{}'.format(val
)
319 return '"{}"'.format(val
)
322 for key
, val
in block
.items():
323 if key
== 'block_name':
325 elif key
== '_blocks_':
327 conf_str
+= GaneshaConfParser
.write_block(blo
, depth
)
329 conf_str
+= GaneshaConfParser
._indentation
(depth
)
330 conf_str
+= '{} = {};\n'.format(key
, format_val(key
, val
))
334 def write_block(block
, depth
):
335 if block
['block_name'] == "%url":
336 return '%url "{}"\n\n'.format(block
['value'])
339 conf_str
+= GaneshaConfParser
._indentation
(depth
)
340 conf_str
+= format(block
['block_name'])
342 conf_str
+= GaneshaConfParser
.write_block_body(block
, depth
+1)
343 conf_str
+= GaneshaConfParser
._indentation
(depth
)
348 def write_conf(blocks
):
349 if not isinstance(blocks
, list):
353 conf_str
+= GaneshaConfParser
.write_block(block
, 0)
358 def __init__(self
, name
):
362 def validate_path(cls
, _
):
363 raise NotImplementedError()
366 raise NotImplementedError()
369 raise NotImplementedError()
371 def create_path(self
, path
):
372 raise NotImplementedError()
375 def from_fsal_block(fsal_block
):
376 if fsal_block
['name'] == "CEPH":
377 return CephFSFSal
.from_fsal_block(fsal_block
)
378 if fsal_block
['name'] == 'RGW':
379 return RGWFSal
.from_fsal_block(fsal_block
)
382 def to_fsal_block(self
):
383 raise NotImplementedError()
386 def from_dict(fsal_dict
):
387 if fsal_dict
['name'] == "CEPH":
388 return CephFSFSal
.from_dict(fsal_dict
)
389 if fsal_dict
['name'] == 'RGW':
390 return RGWFSal
.from_dict(fsal_dict
)
394 raise NotImplementedError()
398 def __init__(self
, name
, rgw_user_id
, access_key
, secret_key
):
399 super(RGWFSal
, self
).__init
__(name
)
400 self
.rgw_user_id
= rgw_user_id
401 self
.access_key
= access_key
402 self
.secret_key
= secret_key
405 def validate_path(cls
, path
):
406 return path
== "/" or re
.match(r
'^[^/><|&()#?]+$', path
)
409 if not self
.rgw_user_id
:
410 raise NFSException('RGW user must be specified')
412 if not RgwClient
.admin_instance().user_exists(self
.rgw_user_id
):
413 raise NFSException("RGW user '{}' does not exist"
414 .format(self
.rgw_user_id
))
417 keys
= RgwClient
.admin_instance().get_user_keys(self
.rgw_user_id
)
418 self
.access_key
= keys
['access_key']
419 self
.secret_key
= keys
['secret_key']
421 def create_path(self
, path
):
422 if path
== '/': # nothing to do
424 rgw
= RgwClient
.instance(self
.rgw_user_id
)
426 exists
= rgw
.bucket_exists(path
, self
.rgw_user_id
)
427 logger
.debug('Checking existence of RGW bucket "%s" for user "%s": %s',
428 path
, self
.rgw_user_id
, exists
)
429 except RequestException
as exp
:
430 if exp
.status_code
== 403:
431 raise NFSException('Cannot create bucket "{}" as it already '
432 'exists, and belongs to other user.'
436 logger
.info('Creating new RGW bucket "%s" for user "%s"', path
,
438 rgw
.create_bucket(path
)
441 def from_fsal_block(cls
, fsal_block
):
442 return cls(fsal_block
['name'],
443 fsal_block
['user_id'],
444 fsal_block
['access_key_id'],
445 fsal_block
['secret_access_key'])
447 def to_fsal_block(self
):
449 'block_name': 'FSAL',
451 'user_id': self
.rgw_user_id
,
452 'access_key_id': self
.access_key
,
453 'secret_access_key': self
.secret_key
457 def from_dict(cls
, fsal_dict
):
458 return cls(fsal_dict
['name'], fsal_dict
['rgw_user_id'], None, None)
463 'rgw_user_id': self
.rgw_user_id
467 class CephFSFSal(FSal
):
468 def __init__(self
, name
, user_id
=None, fs_name
=None, sec_label_xattr
=None,
470 super(CephFSFSal
, self
).__init
__(name
)
471 self
.fs_name
= fs_name
472 self
.user_id
= user_id
473 self
.sec_label_xattr
= sec_label_xattr
474 self
.cephx_key
= cephx_key
477 def validate_path(cls
, path
):
478 return re
.match(r
'^/[^><|&()?]*$', path
)
481 if self
.user_id
and self
.user_id
not in CephX
.list_clients():
482 raise NFSException("cephx user '{}' does not exist"
483 .format(self
.user_id
))
487 self
.cephx_key
= CephX
.get_client_key(self
.user_id
)
489 def create_path(self
, path
):
490 cfs
= CephFS(self
.fs_name
)
496 def from_fsal_block(cls
, fsal_block
):
497 return cls(fsal_block
['name'],
498 fsal_block
.get('user_id', None),
499 fsal_block
.get('filesystem', None),
500 fsal_block
.get('sec_label_xattr', None),
501 fsal_block
.get('secret_access_key', None))
503 def to_fsal_block(self
):
505 'block_name': 'FSAL',
509 result
['user_id'] = self
.user_id
511 result
['filesystem'] = self
.fs_name
512 if self
.sec_label_xattr
:
513 result
['sec_label_xattr'] = self
.sec_label_xattr
515 result
['secret_access_key'] = self
.cephx_key
519 def from_dict(cls
, fsal_dict
):
520 return cls(fsal_dict
['name'], fsal_dict
['user_id'],
521 fsal_dict
['fs_name'], fsal_dict
['sec_label_xattr'], None)
526 'user_id': self
.user_id
,
527 'fs_name': self
.fs_name
,
528 'sec_label_xattr': self
.sec_label_xattr
532 class Client(object):
533 def __init__(self
, addresses
, access_type
=None, squash
=None):
534 self
.addresses
= addresses
535 self
.access_type
= access_type
536 self
.squash
= GaneshaConf
.format_squash(squash
)
539 def from_client_block(cls
, client_block
):
540 addresses
= client_block
['clients']
541 if not isinstance(addresses
, list):
542 addresses
= [addresses
]
543 return cls(addresses
,
544 client_block
.get('access_type', None),
545 client_block
.get('squash', None))
547 def to_client_block(self
):
549 'block_name': 'CLIENT',
550 'clients': self
.addresses
,
553 result
['access_type'] = self
.access_type
555 result
['squash'] = self
.squash
559 def from_dict(cls
, client_dict
):
560 return cls(client_dict
['addresses'], client_dict
['access_type'],
561 client_dict
['squash'])
565 'addresses': self
.addresses
,
566 'access_type': self
.access_type
,
567 'squash': self
.squash
571 class Export(object):
572 # pylint: disable=R0902
573 def __init__(self
, export_id
, path
, fsal
, cluster_id
, daemons
, pseudo
=None,
574 tag
=None, access_type
=None, squash
=None,
575 attr_expiration_time
=None, security_label
=False,
576 protocols
=None, transports
=None, clients
=None):
577 self
.export_id
= export_id
578 self
.path
= GaneshaConf
.format_path(path
)
580 self
.cluster_id
= cluster_id
581 self
.daemons
= set(daemons
)
582 self
.pseudo
= GaneshaConf
.format_path(pseudo
)
584 self
.access_type
= access_type
585 self
.squash
= GaneshaConf
.format_squash(squash
)
586 if attr_expiration_time
is None:
587 self
.attr_expiration_time
= 0
589 self
.attr_expiration_time
= attr_expiration_time
590 self
.security_label
= security_label
591 self
.protocols
= {GaneshaConf
.format_protocol(p
) for p
in protocols
}
592 self
.transports
= set(transports
)
593 self
.clients
= clients
596 # pylint: disable=R0912
597 if not self
.fsal
.validate_path(self
.path
):
598 raise NFSException("Export path ({}) is invalid.".format(self
.path
))
600 if not self
.protocols
:
602 "No NFS protocol version specified for the export.")
604 if not self
.transports
:
606 "No network transport type specified for the export.")
608 for t
in self
.transports
:
609 match
= re
.match(r
'^TCP$|^UDP$', t
)
612 "'{}' is an invalid network transport type identifier"
617 if 4 in self
.protocols
:
620 "Pseudo path is required when NFSv4 protocol is used")
621 match
= re
.match(r
'^/[^><|&()]*$', self
.pseudo
)
624 "Export pseudo path ({}) is invalid".format(self
.pseudo
))
627 match
= re
.match(r
'^[^/><|:&()]+$', self
.tag
)
630 "Export tag ({}) is invalid".format(self
.tag
))
632 if self
.fsal
.name
== 'RGW' and 4 not in self
.protocols
and not self
.tag
:
634 "Tag is mandatory for RGW export when using only NFSv3")
637 def from_export_block(cls
, export_block
, cluster_id
, defaults
):
638 logger
.debug("parsing export block: %s", export_block
)
640 fsal_block
= [b
for b
in export_block
['_blocks_']
641 if b
['block_name'] == "FSAL"]
643 protocols
= export_block
.get('protocols', defaults
['protocols'])
644 if not isinstance(protocols
, list):
645 protocols
= [protocols
]
647 transports
= export_block
.get('transports', defaults
['transports'])
648 if not isinstance(transports
, list):
649 transports
= [transports
]
651 client_blocks
= [b
for b
in export_block
['_blocks_']
652 if b
['block_name'] == "CLIENT"]
654 return cls(export_block
['export_id'],
655 export_block
['path'],
656 FSal
.from_fsal_block(fsal_block
[0]),
659 export_block
.get('pseudo', None),
660 export_block
.get('tag', None),
661 export_block
.get('access_type', defaults
['access_type']),
662 export_block
.get('squash', defaults
['squash']),
663 export_block
.get('attr_expiration_time', None),
664 export_block
.get('security_label', False),
667 [Client
.from_client_block(client
)
668 for client
in client_blocks
])
670 def to_export_block(self
, defaults
):
671 # pylint: disable=too-many-branches
673 'block_name': 'EXPORT',
674 'export_id': self
.export_id
,
678 result
['pseudo'] = self
.pseudo
680 result
['tag'] = self
.tag
681 if 'access_type' not in defaults \
682 or self
.access_type
!= defaults
['access_type']:
683 result
['access_type'] = self
.access_type
684 if 'squash' not in defaults
or self
.squash
!= defaults
['squash']:
685 result
['squash'] = self
.squash
686 if self
.fsal
.name
== 'CEPH':
687 result
['attr_expiration_time'] = self
.attr_expiration_time
688 result
['security_label'] = self
.security_label
689 if 'protocols' not in defaults
:
690 result
['protocols'] = [p
for p
in self
.protocols
]
692 def_proto
= defaults
['protocols']
693 if not isinstance(def_proto
, list):
694 def_proto
= set([def_proto
])
695 if self
.protocols
!= def_proto
:
696 result
['protocols'] = [p
for p
in self
.protocols
]
697 if 'transports' not in defaults
:
698 result
['transports'] = [t
for t
in self
.transports
]
700 def_transp
= defaults
['transports']
701 if not isinstance(def_transp
, list):
702 def_transp
= set([def_transp
])
703 if self
.transports
!= def_transp
:
704 result
['transports'] = [t
for t
in self
.transports
]
706 result
['_blocks_'] = [self
.fsal
.to_fsal_block()]
707 result
['_blocks_'].extend([client
.to_client_block()
708 for client
in self
.clients
])
712 def from_dict(cls
, export_id
, ex_dict
, old_export
=None):
713 return cls(export_id
,
715 FSal
.from_dict(ex_dict
['fsal']),
716 ex_dict
['cluster_id'],
720 ex_dict
['access_type'],
722 old_export
.attr_expiration_time
if old_export
else None,
723 ex_dict
['security_label'],
724 ex_dict
['protocols'],
725 ex_dict
['transports'],
726 [Client
.from_dict(client
) for client
in ex_dict
['clients']])
730 'export_id': self
.export_id
,
732 'fsal': self
.fsal
.to_dict(),
733 'cluster_id': self
.cluster_id
,
734 'daemons': sorted([d
for d
in self
.daemons
]),
735 'pseudo': self
.pseudo
,
737 'access_type': self
.access_type
,
738 'squash': self
.squash
,
739 'security_label': self
.security_label
,
740 'protocols': sorted([p
for p
in self
.protocols
]),
741 'transports': sorted([t
for t
in self
.transports
]),
742 'clients': [client
.to_dict() for client
in self
.clients
]
746 class ClusterType(object):
748 # Ganesha clusters deployed by the Orchestrator.
749 ORCHESTRATOR
= 'orchestrator'
751 # Ganesha clusters deployed manually by the user. Specified by using the
752 # GANESHA_CLUSTERS_RADOS_POOL_NAMESPACE setting.
756 class GaneshaConf(object):
757 # pylint: disable=R0902
759 def __init__(self
, cluster_id
, rados_pool
, rados_namespace
, daemon_confs
=None):
760 self
.cluster_id
= cluster_id
761 self
.rados_pool
= rados_pool
762 self
.rados_namespace
= rados_namespace
763 self
.daemon_confs
= daemon_confs
if daemon_confs
is not None else []
764 self
.export_conf_blocks
= [] # type: ignore
765 self
.daemons_conf_blocks
= {} # type: ignore
769 self
._read
_raw
_config
()
772 def_block
= [b
for b
in self
.export_conf_blocks
773 if b
['block_name'] == "EXPORT_DEFAULTS"]
774 self
.export_defaults
= def_block
[0] if def_block
else {}
775 self
._defaults
= self
.ganesha_defaults(self
.export_defaults
)
777 for export_block
in [block
for block
in self
.export_conf_blocks
778 if block
['block_name'] == "EXPORT"]:
779 export
= Export
.from_export_block(export_block
, cluster_id
,
781 self
.exports
[export
.export_id
] = export
783 # link daemons to exports
784 self
._link
_daemons
_to
_exports
()
786 def _link_daemons_to_exports(self
):
787 raise NotImplementedError()
790 def instance(cls
, cluster_id
):
791 cluster
= Ganesha
.get_cluster(cluster_id
)
792 if cluster
['type'] == ClusterType
.ORCHESTRATOR
:
793 return GaneshaConfOrchestrator(cluster_id
, cluster
['pool'], cluster
['namespace'],
794 [cluster
['daemon_conf']])
795 if cluster
['type'] == ClusterType
.USER
:
796 return GaneshaConfUser(cluster_id
, cluster
['pool'], cluster
['namespace'])
797 raise NFSException('Unknown cluster type `{}` for cluster `{}`'.format(
798 cluster
['type'], cluster_id
))
800 def _read_raw_config(self
):
802 def _read_rados_obj(_obj
):
803 size
, _
= _obj
.stat()
804 return _obj
.read(size
).decode("utf-8")
806 with mgr
.rados
.open_ioctx(self
.rados_pool
) as ioctx
:
807 if self
.rados_namespace
:
808 ioctx
.set_namespace(self
.rados_namespace
)
809 objs
= ioctx
.list_objects()
811 if obj
.key
.startswith("export-"):
812 raw_config
= _read_rados_obj(obj
)
813 logger
.debug("read export configuration from rados "
814 "object %s/%s/%s:\n%s", self
.rados_pool
,
815 self
.rados_namespace
, obj
.key
, raw_config
)
816 self
.export_conf_blocks
.extend(
817 GaneshaConfParser(raw_config
).parse())
818 elif not self
.daemon_confs
and obj
.key
.startswith("conf-"):
819 # Read all `conf-xxx` for daemon configs.
820 raw_config
= _read_rados_obj(obj
)
821 logger
.debug("read daemon configuration from rados "
822 "object %s/%s/%s:\n%s", self
.rados_pool
,
823 self
.rados_namespace
, obj
.key
, raw_config
)
824 idx
= obj
.key
.find('-')
825 self
.daemons_conf_blocks
[obj
.key
[idx
+1:]] = \
826 GaneshaConfParser(raw_config
).parse()
828 if self
.daemon_confs
:
829 # When daemon configs are provided.
830 for conf
in self
.daemon_confs
:
831 size
, _
= ioctx
.stat(conf
)
832 raw_config
= ioctx
.read(conf
, size
).decode("utf-8")
833 logger
.debug("read daemon configuration from rados "
834 "object %s/%s/%s:\n%s", self
.rados_pool
,
835 self
.rados_namespace
, conf
, raw_config
)
836 self
.daemons_conf_blocks
[conf
] = \
837 GaneshaConfParser(raw_config
).parse()
839 def _write_raw_config(self
, conf_block
, obj
):
840 raw_config
= GaneshaConfParser
.write_conf(conf_block
)
841 with mgr
.rados
.open_ioctx(self
.rados_pool
) as ioctx
:
842 if self
.rados_namespace
:
843 ioctx
.set_namespace(self
.rados_namespace
)
844 ioctx
.write_full(obj
, raw_config
.encode('utf-8'))
846 "write configuration into rados object %s/%s/%s:\n%s",
847 self
.rados_pool
, self
.rados_namespace
, obj
, raw_config
)
850 def ganesha_defaults(cls
, export_defaults
):
853 https://github.com/nfs-ganesha/nfs-ganesha/blob/next/src/config_samples/export.txt
856 'access_type': export_defaults
.get('access_type', 'NONE'),
857 'protocols': export_defaults
.get('protocols', [3, 4]),
858 'transports': export_defaults
.get('transports', ['TCP', 'UDP']),
859 'squash': export_defaults
.get('squash', 'root_squash')
863 def format_squash(cls
, squash
):
866 if squash
.lower() in ["no_root_squash", "noidsquash", "none"]:
867 return "no_root_squash"
868 if squash
.lower() in ["rootid", "root_id_squash", "rootidsquash"]:
869 return "root_id_squash"
870 if squash
.lower() in ["root", "root_squash", "rootsquash"]:
872 if squash
.lower() in ["all", "all_squash", "allsquash",
873 "all_anonymous", "allanonymous"]:
875 logger
.error("could not parse squash value: %s", squash
)
876 raise NFSException("'{}' is an invalid squash option".format(squash
))
879 def format_protocol(cls
, protocol
):
880 if str(protocol
) in ["NFSV3", "3", "V3", "NFS3"]:
882 if str(protocol
) in ["NFSV4", "4", "V4", "NFS4"]:
884 logger
.error("could not parse protocol value: %s", protocol
)
885 raise NFSException("'{}' is an invalid NFS protocol version identifier"
889 def format_path(cls
, path
):
892 if len(path
) > 1 and path
[-1] == '/':
896 def validate(self
, export
: Export
):
899 if 4 in export
.protocols
: # NFSv4 protocol
902 for ex
in self
.list_exports():
903 if export
.tag
and ex
.tag
== export
.tag
:
905 "Another export exists with the same tag: {}"
908 if export
.pseudo
and ex
.pseudo
== export
.pseudo
:
910 "Another export exists with the same pseudo path: {}"
911 .format(export
.pseudo
))
916 if export
.pseudo
[:export
.pseudo
.rfind('/')+1].startswith(ex
.pseudo
):
917 if export
.pseudo
[len(ex
.pseudo
)] == '/':
918 if len(ex
.pseudo
) > len_prefix
:
919 len_prefix
= len(ex
.pseudo
)
923 # validate pseudo path
924 idx
= len(parent_export
.pseudo
) # type: ignore
925 idx
= idx
+ 1 if idx
> 1 else idx
926 real_path
= "{}/{}".format(
927 parent_export
.path
# type: ignore
928 if len(parent_export
.path
) > 1 else "", # type: ignore
930 if export
.fsal
.name
== 'CEPH':
932 if export
.path
!= real_path
and not cfs
.dir_exists(real_path
):
934 "Pseudo path ({}) invalid, path {} does not exist."
935 .format(export
.pseudo
, real_path
))
937 def _gen_export_id(self
):
938 exports
= sorted(self
.exports
)
947 def _persist_daemon_configuration(self
):
948 raise NotImplementedError()
950 def _save_export(self
, export
):
951 self
.validate(export
)
952 export
.fsal
.create_path(export
.path
)
953 export
.fsal
.fill_keys()
954 self
.exports
[export
.export_id
] = export
955 conf_block
= export
.to_export_block(self
.export_defaults
)
956 self
._write
_raw
_config
(conf_block
, "export-{}".format(export
.export_id
))
957 self
._persist
_daemon
_configuration
()
959 def _delete_export(self
, export_id
):
960 self
._persist
_daemon
_configuration
()
961 with mgr
.rados
.open_ioctx(self
.rados_pool
) as ioctx
:
962 if self
.rados_namespace
:
963 ioctx
.set_namespace(self
.rados_namespace
)
964 ioctx
.remove_object("export-{}".format(export_id
))
966 def list_exports(self
):
967 return [ex
for _
, ex
in self
.exports
.items()]
969 def create_export(self
, ex_dict
):
970 ex_id
= self
._gen
_export
_id
()
971 export
= Export
.from_dict(ex_id
, ex_dict
)
972 self
._save
_export
(export
)
975 def has_export(self
, export_id
):
976 return export_id
in self
.exports
978 def update_export(self
, ex_dict
):
979 if ex_dict
['export_id'] not in self
.exports
:
981 old_export
= self
.exports
[ex_dict
['export_id']]
982 del self
.exports
[ex_dict
['export_id']]
983 export
= Export
.from_dict(ex_dict
['export_id'], ex_dict
, old_export
)
984 self
._save
_export
(export
)
985 self
.exports
[export
.export_id
] = export
988 def remove_export(self
, export_id
):
989 if export_id
not in self
.exports
:
991 export
= self
.exports
[export_id
]
992 del self
.exports
[export_id
]
993 self
._delete
_export
(export_id
)
996 def get_export(self
, export_id
):
997 if export_id
in self
.exports
:
998 return self
.exports
[export_id
]
1001 def list_daemons(self
) -> List
[Dict
[str, Any
]]:
1002 raise NotImplementedError()
1004 def list_daemon_confs(self
):
1005 return self
.daemons_conf_blocks
.keys()
1007 def reload_daemons(self
, daemons
):
1008 with mgr
.rados
.open_ioctx(self
.rados_pool
) as ioctx
:
1009 if self
.rados_namespace
:
1010 ioctx
.set_namespace(self
.rados_namespace
)
1011 for daemon_id
in daemons
:
1012 ioctx
.notify("conf-{}".format(daemon_id
))
1015 class GaneshaConfOrchestrator(GaneshaConf
):
1017 def _get_orch_nfs_instances(cls
,
1018 service_name
: Optional
[str] = None) -> List
[DaemonDescription
]:
1020 return OrchClient
.instance().services
.\
1021 list_daemons(service_name
=service_name
, daemon_type
="nfs")
1022 except (RuntimeError, OrchestratorError
, ImportError):
1025 def _link_daemons_to_exports(self
):
1026 instances
= self
._get
_orch
_nfs
_instances
('nfs.{}'.format(self
.cluster_id
))
1027 daemon_ids
= {instance
.daemon_id
for instance
in instances
}
1028 for _
, daemon_blocks
in self
.daemons_conf_blocks
.items():
1029 for block
in daemon_blocks
:
1030 if block
['block_name'] == "%url":
1031 rados_url
= block
['value']
1032 _
, _
, obj
= Ganesha
.parse_rados_url(rados_url
)
1033 if obj
.startswith("export-"):
1034 export_id
= int(obj
[obj
.find('-')+1:])
1035 self
.exports
[export_id
].daemons
.update(daemon_ids
)
1037 def validate(self
, export
: Export
):
1038 daemons_list
= {d
['daemon_id'] for d
in self
.list_daemons()}
1039 if export
.daemons
and set(export
.daemons
) != daemons_list
:
1040 raise NFSException('Export should be linked to all daemons.')
1041 super().validate(export
)
1043 def _persist_daemon_configuration(self
):
1044 daemon_map
= {} # type: ignore
1045 for daemon_id
in self
.list_daemon_confs():
1046 daemon_map
[daemon_id
] = []
1048 for daemon_id
in self
.list_daemon_confs():
1049 for _
, ex
in self
.exports
.items():
1051 daemon_map
[daemon_id
].append({
1052 'block_name': "%url",
1053 'value': Ganesha
.make_rados_url(
1054 self
.rados_pool
, self
.rados_namespace
,
1055 "export-{}".format(ex
.export_id
))
1057 for daemon_id
, conf_blocks
in daemon_map
.items():
1058 self
._write
_raw
_config
(conf_blocks
, daemon_id
)
1060 def list_daemons(self
) -> List
[Dict
[str, Any
]]:
1061 instances
= self
._get
_orch
_nfs
_instances
('nfs.{}'.format(self
.cluster_id
))
1063 'cluster_id': self
.cluster_id
,
1064 'daemon_id': instance
.daemon_id
,
1065 'cluster_type': ClusterType
.ORCHESTRATOR
,
1066 'status': instance
.status
,
1067 'status_desc': instance
.status_desc
1068 } for instance
in instances
]
1070 def reload_daemons(self
, daemons
):
1071 with mgr
.rados
.open_ioctx(self
.rados_pool
) as ioctx
:
1072 if self
.rados_namespace
:
1073 ioctx
.set_namespace(self
.rados_namespace
)
1074 for daemon_id
in self
.list_daemon_confs():
1075 ioctx
.notify(daemon_id
)
1078 class GaneshaConfUser(GaneshaConf
):
1080 def _link_daemons_to_exports(self
):
1081 for daemon_id
, daemon_blocks
in self
.daemons_conf_blocks
.items():
1082 for block
in daemon_blocks
:
1083 if block
['block_name'] == "%url":
1084 rados_url
= block
['value']
1085 _
, _
, obj
= Ganesha
.parse_rados_url(rados_url
)
1086 if obj
.startswith("export-"):
1087 export_id
= int(obj
[obj
.find('-')+1:])
1088 self
.exports
[export_id
].daemons
.add(daemon_id
)
1090 def validate(self
, export
: Export
):
1091 daemons_list
= [d
['daemon_id'] for d
in self
.list_daemons()]
1092 for daemon_id
in export
.daemons
:
1093 if daemon_id
not in daemons_list
:
1094 raise NFSException("Daemon '{}' does not exist".format(daemon_id
))
1095 super().validate(export
)
1097 def _persist_daemon_configuration(self
):
1098 daemon_map
= {} # type: ignore
1099 for daemon_id
in self
.list_daemon_confs():
1100 daemon_map
[daemon_id
] = []
1102 for _
, ex
in self
.exports
.items():
1103 for daemon
in ex
.daemons
:
1104 daemon_map
[daemon
].append({
1105 'block_name': "%url",
1106 'value': Ganesha
.make_rados_url(
1107 self
.rados_pool
, self
.rados_namespace
,
1108 "export-{}".format(ex
.export_id
))
1110 for daemon_id
, conf_blocks
in daemon_map
.items():
1111 self
._write
_raw
_config
(conf_blocks
, "conf-{}".format(daemon_id
))
1113 def list_daemons(self
) -> List
[Dict
[str, Any
]]:
1115 'cluster_id': self
.cluster_id
,
1116 'cluster_type': ClusterType
.USER
,
1117 'daemon_id': daemon_id
,
1119 'status_desc': 'running'
1120 } for daemon_id
in self
.list_daemon_confs()]