]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
pybind/nfs: support custom "log" block wip-fs-suite-ganesha-tests-x7
authorVenky Shankar <vshankar@redhat.com>
Tue, 14 Oct 2025 09:10:40 +0000 (09:10 +0000)
committerVenky Shankar <vshankar@redhat.com>
Tue, 14 Oct 2025 09:11:25 +0000 (09:11 +0000)
Signed-off-by: Venky Shankar <vshankar@redhat.com>
src/pybind/mgr/nfs/export.py
src/pybind/mgr/nfs/ganesha_conf.py

index 1f0d4b26132f38e71449f4f28d3572b74d0991ff..83b8bf0f7be6cf812b363a56574464eed731a9cb 100644 (file)
@@ -29,6 +29,7 @@ from .ganesha_conf import (
     RGWFSAL,
     RawBlock,
     CephBlock,
+    LogBlock,
     format_block)
 from .exception import NFSException, NFSInvalidOperation, FSNotFound, NFSObjectNotFound
 from .utils import (
@@ -218,12 +219,14 @@ class AppliedExportResults:
         return self.status
 
 class GaneshaExport:
-    # currently, EXPORT and CEPH block.
+    # EXPORT, CEPH and LOG block.
     def __init__(self,
                  export: Export,
-                 ceph_block: Optional[CephBlock] = None) -> None:
+                 ceph_block: Optional[CephBlock] = None,
+                 log_block: Optional[LogBlock] = None) -> None:
         self.export = export
         self.ceph_block = ceph_block
+        self.log_block = log_block
 
     # frequently uesd properties so that much of the code that now
     # has moved to using this class can still continue to acess via
@@ -250,18 +253,21 @@ class GaneshaExport:
 
     def to_dict(self, full=False) -> Dict[str, Any]:
         export_dict = self.export.to_dict()
-        if not full or not self.ceph_block:
+        if not full or (not self.ceph_block and not self.log_block):
             return export_dict
-        ge_dict = {
-            'export': export_dict,
-            'ceph': self.ceph_block.to_dict()
-            }
+        ge_dict = {'export': export_dict}
+        if self.ceph_block:
+            ge_dict['ceph'] = self.ceph_block.to_dict()
+        if self.log_block:
+            ge_dict['log'] = self.log_block.to_dict()
         return ge_dict
 
     def to_export_block(self):
         block_str = format_block(self.export.to_export_block())
         if self.ceph_block:
             block_str += format_block(self.ceph_block.to_ceph_block())
+        if self.log_block:
+            block_str += format_block(self.log_block.to_log_block())
         return block_str
 
     def __eq__(self, other: Any) -> bool:
@@ -379,8 +385,10 @@ class ExportMgr:
                 break
         return nid
 
-    def _has_ceph_block(raw_config_parsed: List) -> bool:
-        return len(raw_config_parsed) > 1
+    def _has_ceph_block(raw_config_parsed: Dict) -> bool:
+        return 'CEPH' in raw_config_parsed.keys()
+    def _has_log_block(raw_config_parsed: Dict) -> bool:
+        return 'LOG' in raw_config_parsed.keys()
 
     def _read_raw_config(self, rados_namespace: str) -> None:
         with self.mgr.rados.open_ioctx(self.rados_pool) as ioctx:
@@ -396,16 +404,19 @@ class ExportMgr:
                     log.debug(f'raw_config: {raw_config}')
                     raw_config_parsed = GaneshaConfParser(raw_config).parse()
                     log.debug(f'raw_config_parsed: {raw_config_parsed}')
-                    export_block = raw_config_parsed[0]
-                    # do we have a ceph block?
+                    # mandatory export block
+                    export_block = raw_config_parsed['EXPORT']
+                    # do we have a ceph/log block? (optional)
+                    ceph_block = None
+                    log_block = None
                     if _has_ceph_block(raw_config_parsed):
-                        ceph_block = raw_config_parsed[1]
-                        self.export_conf_objs.append(
+                        ceph_block = raw_config_parsed['CEPH']
+                    if _has_log_block(raw_config_parsed):
+                        log_block = raw_config_parsed['LOG']
+                    self.export_conf_objs.append(
                             GaneshaExport(Export.from_export_block(export_block, rados_namespace),
-                                          CephBlock.from_ceph_block(ceph_block)))
-                    else:
-                        self.export_conf_objs.append(
-                            GaneshaExport(Export.from_export_block(export_block, rados_namespace)))
+                                          CephBlock.from_ceph_block(ceph_block),
+                                          LogBlock.from_log_block(log_block)))
 
     def _save_export(self, cluster_id: str, ganesha_export: GaneshaExport) -> None:
         log.debug('in _save_export')
@@ -462,14 +473,18 @@ class ExportMgr:
                 log.debug(f'raw_config: {raw_config}')
                 raw_config_parsed = GaneshaConfParser(raw_config).parse()
                 log.debug(f'raw_config_parsed: {raw_config_parsed}')
-                export_block = raw_config_parsed[0]
-                # do we have a ceph block?
+                export_block = raw_config_parsed['EXPORT']
+                # do we have a ceph/log block? (optional)
+                ceph_block = None
+                log_block = None
                 if _has_ceph_block(raw_config_parsed):
-                    ceph_block = raw_config_parsed[1]
-                    export = GaneshaExport(Export.from_export_block(export_block, cluster_id),
-                                           CephBlock.from_ceph_block(ceph_block))
-                else:
-                    export = GaneshaExport(Export.from_export_block(export_block, cluster_id))
+                    ceph_block = raw_config_parsed['CEPH']
+                if _has_log_block(raw_config_parsed):
+                    log_block = raw_config_parsed['LOG']
+                self.export_conf_objs.append(
+                    GaneshaExport(Export.from_export_block(export_block, rados_namespace),
+                                  CephBlock.from_ceph_block(ceph_block),
+                                  LogBlock.from_log_block(log_block)))
                 log.debug(f'export: {export}')
                 return export
         except ObjectNotFound:
@@ -661,20 +676,24 @@ class ExportMgr:
 
     def _change_export(self, cluster_id: str, export: Dict,
                        earmark_resolver: Optional[CephFSEarmarkResolver] = None) -> Dict[str, Any]:
-        # if the export json has a ceph section (key), extract it from the export
+        # if the export json has a ceph/log section (key), extract it from the export
         # json to preserver backward compatability.
         ceph_dict = {}
+        log_dict = {}
         if "ceph" in export.keys():
             ceph_dict = export.pop("ceph")
-            if not "export" in export.keys():
-                raise Exception('\'export\' key missing in export json')
+        if "log" in export.keys():
+            log_dict = export.pop("log")
+        if "export" in export.keys():
             export = export.pop("export")
         msg = f'export_dict: {export}'
         log.exception(msg)
         msg = f'ceph_dict: {ceph_dict}'
         log.exception(msg)
+        msg = f'log_dict: {log_dict}'
+        log.exception(msg)
         try:
-            return self._apply_export(cluster_id, export, earmark_resolver, ceph_dict)
+            return self._apply_export(cluster_id, export, earmark_resolver, ceph_dict, log_dict)
         except NotImplementedError as e:
             # in theory, the NotImplementedError here may be raised by a hook back to
             # an orchestration module. If the orchestration module supports it the NFS
@@ -926,7 +945,8 @@ class ExportMgr:
             cluster_id: str,
             new_export_dict: Dict,
             earmark_resolver: Optional[CephFSEarmarkResolver] = None,
-            ceph_dict: Optional[Dict] = {}) -> Dict[str, str]:
+            ceph_dict: Optional[Dict] = {},
+            log_dict: Optional[Dict] = {}) -> Dict[str, str]:
         for k in ['path', 'pseudo']:
             if k not in new_export_dict:
                 raise NFSInvalidOperation(f'Export missing required field {k}')
@@ -972,9 +992,13 @@ class ExportMgr:
         log.debug(f'ceph_dict: {ceph_dict}')
         if ceph_dict:
             ceph_block = CephBlock.from_dict(ceph_dict)
+        log_block = None
+        log.debug(f'log_dict: {log_dict}')
+        if log_dict:
+            log_block = LogBlock.from_dict(log_dict)
 
         # use @ganesha_export in place of @new_export here onwards
-        ganesha_export = GaneshaExport(new_export, ceph_block)
+        ganesha_export = GaneshaExport(new_export, ceph_block, log_block)
 
         if not old_export:
             if new_export.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[1]:  # only for RGW
@@ -997,7 +1021,8 @@ class ExportMgr:
                                             and old_fsal.fs_name == new_fsal.fs_name
                                             and old_export.path == new_export.path
                                             and old_export.pseudo == new_export.pseudo
-                                            and old_export.ceph_block == ganesha_export.ceph_block)
+                                            and old_export.ceph_block == ganesha_export.ceph_block
+                                            and old_export.log_block == ganesha_export.log_block)
 
         if old_export.fsal.name == NFS_GANESHA_SUPPORTED_FSALS[1]:
             old_rgw_fsal = cast(RGWFSAL, old_export.fsal)
index 6946297cd759fd19e287e7334ac0b7d679418346..bd2cef02b3d4dc27d46ce140d844863d61b91ca8 100644 (file)
@@ -113,14 +113,15 @@ class GaneshaConfParser:
                 value = self.stream()[:idx]
                 self.pos += idx + 1
             block_dict = RawBlock('%url', values={'value': value})
-            return block_dict
+            return ('%url', block_dict)
 
-        block_dict = RawBlock(self.parse_block_name().upper())
+        block_name = self.parse_block_name().upper()
+        block_dict = RawBlock(block_name)
         self.parse_block_body(block_dict)
         if self.stream()[0] != '}':
             raise Exception("No closing bracket '}' found at the end of block")
         self.pos += 1
-        return block_dict
+        return (block_name, block_dict)
 
     def parse_parameter_value(self, raw_value: str) -> Any:
         if raw_value.find(',') != -1:
@@ -164,7 +165,7 @@ class GaneshaConfParser:
                 self.parse_stanza(block_dict)
             elif is_lbracket and ((is_semicolon and not is_semicolon_lt_lbracket)
                                   or (not is_semicolon)):
-                block_dict.blocks.append(self.parse_block_or_section())
+                block_dict.blocks.append(self.parse_block_or_section()[1])
             else:
                 raise Exception("Malformed stanza: no semicolon found.")
 
@@ -172,9 +173,10 @@ class GaneshaConfParser:
                 raise Exception("Infinite loop while parsing block content")
 
     def parse(self) -> List[RawBlock]:
-        blocks = []
+        blocks = {}
         while self.stream():
-            blocks.append(self.parse_block_or_section())
+            (block_name, block) = self.parse_block_or_section()
+            blocks[block_name] = block
         return blocks
 
 
@@ -381,7 +383,7 @@ class CephBlock:
         return result
 
     @classmethod
-    def from_dict(cls, ex_dict: Dict[str, Any]) -> 'Export':
+    def from_dict(cls, ex_dict: Dict[str, Any]) -> 'CephBlock':
         return cls(ex_dict.get('async', False),
                    ex_dict.get('zerocopy', False))
 
@@ -397,6 +399,117 @@ class CephBlock:
             return False
         return self.to_dict() == other.to_dict()
 
+class Facility:
+    def __init(self,
+               name: str,
+               destination: str,
+               enable: str):
+        self.name = name
+        self.destination = destination
+        self.enable = enable
+
+    @classmethod
+    def from_facility_block(cls, facility: RawBlock) -> 'Facility':
+        return cls(facility.values['name'],
+                   facility.values['destination'], facility.values['enable'])
+
+    def to_facility_block(self) -> RawBlock:
+        result = RawBlock("FACILITY", values={'name': self.name,
+                                              'destination': self.destination,
+                                              'enable': self.enable})
+        return result
+
+    @classmethod
+    def from_dict(cls, ex_dict: Dict[str, Any]) -> 'Facility':
+        return cls(ex_dict['name'], ex_dict['destination'], ex_dict['enable'])
+
+    def to_dict(self) -> Dict[str, Any]:
+        values = {
+            'name': self.name,
+            'destination': self.destination,
+            'enable': self.enable
+        }
+        return values
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, Facility):
+            return False
+        return self.to_dict() == other.to_dict()
+
+class Components:
+    def __init(self,
+               fsal: str,
+               nfsv4: str):
+        self.fsal = fsal
+        self.nfsv4 = nfsv4
+
+    @classmethod
+    def from_components_block(cls, components: RawBlock) -> 'Components':
+        return cls(components.values['fsal'], components.values['nfsv4'])
+
+    def to_components_block(self) -> RawBlock:
+        result = RawBlock("COMPONENTS", values={'fsal': self.fsal, 'nfsv4': self.nfsv4})
+        return result
+
+    @classmethod
+    def from_dict(cls, ex_dict: Dict[str, Any]) -> 'Components':
+        return cls(ex_dict['fsal'], ex_dict['nfsv4'])
+
+    def to_dict(self) -> Dict[str, Any]:
+        values = {
+            'fsal': self.fsal,
+            'nfsv4': self.nfsv4
+        }
+        return values
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, Components):
+            return False
+        return self.to_dict() == other.to_dict()
+
+class LogBlock:
+    def __init__(self,
+                 default_log_level: str,
+                 components: Components,
+                 facility: Facility):
+        self.default_log_level = default_log_level
+        self.components = components
+        self.facility = facility
+
+    @classmethod
+    def from_log_block(cls, log_block: RawBlock) -> 'LogBlock':
+        return cls(log_block.values.get('default_log_level', None),
+                   Components.from_components_block(self.components),
+                   Facility.from_facility_block(self.facility))
+
+    def to_log_block(self) -> RawBlock:
+        result = RawBlock("LOG", values={'default_log_level': self.default_log_level})
+        result.blocks = [
+            self.components.to_components_block()
+            ] + [
+                self.facility.to_facility_block()
+            ]
+        return result
+
+    @classmethod
+    def from_dict(cls, ex_dict: Dict[str, Any]) -> 'LogBlock':
+        return cls(ex_dict['default_log_level'],
+                   Components.from_dict(ex_dict['components']),
+                   Facility.fron_dict(ex_dict['facility']))
+
+    def to_dict(self) -> Dict[str, Any]:
+        values = {
+            'default_log_level': self.default_log_level,
+            'components': self.components.to_dict(),
+            'facility': self.facility.to_dict()
+        }
+        return values
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, LogBlock):
+            return False
+        return self.to_dict() == other.to_dict()
+
 class Export:
     def __init__(
             self,