]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/cephadm: support for nfs backed by VIP
authorAdam King <adking@redhat.com>
Wed, 20 Jul 2022 21:55:07 +0000 (17:55 -0400)
committerAdam King <adking@redhat.com>
Mon, 1 May 2023 19:45:11 +0000 (15:45 -0400)
Fixes: https://tracker.ceph.com/issues/55663
Signed-off-by: Adam King <adking@redhat.com>
src/pybind/mgr/cephadm/module.py
src/pybind/mgr/cephadm/serve.py
src/pybind/mgr/cephadm/services/cephadmservice.py
src/pybind/mgr/cephadm/services/ingress.py
src/pybind/mgr/cephadm/services/nfs.py
src/pybind/mgr/nfs/cluster.py
src/pybind/mgr/nfs/module.py
src/pybind/mgr/orchestrator/module.py
src/python-common/ceph/deployment/service_spec.py

index 9fbbe6f2fcef59dba05c63d8ce76eff67aecb486..777e80acf1b1178b7c14515ad9b16a050c1ff602 100644 (file)
@@ -2042,7 +2042,9 @@ Then run the following:
             )
             if spec.service_type == 'ingress':
                 # ingress has 2 daemons running per host
-                sm[nm].size *= 2
+                # but only if it's the full ingress service, not for keepalive-only
+                if not cast(IngressSpec, spec).keepalive_only:
+                    sm[nm].size *= 2
 
         # factor daemons into status
         for h, dm in self.cache.get_daemons_with_volatile_status():
index 40bc2025ac6f5e726f8cefb49a93a59814668efb..54633e41d9419d231adce432c2d7916f93d826b8 100644 (file)
@@ -715,8 +715,8 @@ class CephadmServe:
                 else None
             ),
             allow_colo=svc.allow_colo(),
-            primary_daemon_type=svc.primary_daemon_type(),
-            per_host_daemon_type=svc.per_host_daemon_type(),
+            primary_daemon_type=svc.primary_daemon_type(spec),
+            per_host_daemon_type=svc.per_host_daemon_type(spec),
             rank_map=rank_map,
         )
 
index 9272bccdd9f1c00c25e82dab57fa6f72ecc38752..62652382c3f9fcba61176f4efa5b0855135e9225 100644 (file)
@@ -169,13 +169,13 @@ class CephadmService(metaclass=ABCMeta):
         """
         return False
 
-    def primary_daemon_type(self) -> str:
+    def primary_daemon_type(self, spec: Optional[ServiceSpec] = None) -> str:
         """
         This is the type of the primary (usually only) daemon to be deployed.
         """
         return self.TYPE
 
-    def per_host_daemon_type(self) -> Optional[str]:
+    def per_host_daemon_type(self, spec: Optional[ServiceSpec] = None) -> Optional[str]:
         """
         If defined, this type of daemon will be deployed once for each host
         containing one or more daemons of the primary type.
index 2d572738e0946630827bf587f6667eb2f93b52be..7523bc086cee667a64f0c1e1ba6d1e4c4187f2ed 100644 (file)
@@ -4,7 +4,7 @@ import random
 import string
 from typing import List, Dict, Any, Tuple, cast, Optional
 
-from ceph.deployment.service_spec import IngressSpec
+from ceph.deployment.service_spec import ServiceSpec, IngressSpec
 from mgr_util import build_url
 from cephadm import utils
 from orchestrator import OrchestratorError, DaemonDescription
@@ -17,10 +17,25 @@ class IngressService(CephService):
     TYPE = 'ingress'
     MAX_KEEPALIVED_PASS_LEN = 8
 
-    def primary_daemon_type(self) -> str:
+    def primary_daemon_type(self, spec: Optional[ServiceSpec] = None) -> str:
+        if spec:
+            ispec = cast(IngressSpec, spec)
+            # in keepalive only setups, we are only deploying keepalived,
+            # so that should be marked as the primary daemon type. Otherwise,
+            # we consider haproxy to be the primary.
+            if hasattr(spec, 'keepalive_only') and ispec.keepalive_only:
+                return 'keepalived'
         return 'haproxy'
 
-    def per_host_daemon_type(self) -> Optional[str]:
+    def per_host_daemon_type(self, spec: Optional[ServiceSpec] = None) -> Optional[str]:
+        if spec:
+            ispec = cast(IngressSpec, spec)
+            # if we are using "keepalive_only" mode on this ingress service
+            # we are only deploying keepalived daemons, so there should
+            # only be a primary daemon type and the per host daemon type
+            # should be empty
+            if hasattr(spec, 'keepalive_only') and ispec.keepalive_only:
+                return None
         return 'keepalived'
 
     def prepare_create(
@@ -218,7 +233,7 @@ class IngressService(CephService):
 
         daemons = self.mgr.cache.get_daemons_by_service(spec.service_name())
 
-        if not daemons:
+        if not daemons and not spec.keepalive_only:
             raise OrchestratorError(
                 f'Failed to generate keepalived.conf: No daemons deployed for {spec.service_name()}')
 
index ad7c442eebd751a4c49734aa5ad659a5dcb840c7..0bd670377d01e25df7f480948975898582e7ebcd 100644 (file)
@@ -92,6 +92,9 @@ class NFSService(CephService):
         # create the RGW keyring
         rgw_user = f'{rados_user}-rgw'
         rgw_keyring = self.create_rgw_keyring(daemon_spec)
+        bind_addr = spec.virtual_ip if spec.virtual_ip else (daemon_spec.ip if daemon_spec.ip else '')
+        if not bind_addr:
+            logger.warning(f'Bind address in {daemon_type}.{daemon_id}\'s ganesha conf is defaulting to empty')
 
         # generate the ganesha config
         def get_ganesha_conf() -> str:
@@ -104,7 +107,7 @@ class NFSService(CephService):
                 "url": f'rados://{POOL_NAME}/{spec.service_id}/{spec.rados_config_name()}',
                 # fall back to default NFS port if not present in daemon_spec
                 "port": daemon_spec.ports[0] if daemon_spec.ports else 2049,
-                "bind_addr": daemon_spec.ip if daemon_spec.ip else '',
+                "bind_addr": bind_addr,
             }
             return self.mgr.template.render('services/nfs/ganesha.conf.j2', context)
 
index 1e87d0d1afbd03ecb2a47f3c52ff2bb1df68b792..3bd36ff3e9958f5f879dd03ef6b8cfdc8adf69c3 100644 (file)
@@ -9,6 +9,7 @@ from ceph.deployment.service_spec import NFSServiceSpec, PlacementSpec, IngressS
 from object_format import ErrorResponse
 
 import orchestrator
+from orchestrator.module import IngressType
 
 from .exception import NFSInvalidOperation, ClusterNotFound
 from .utils import (
@@ -60,8 +61,9 @@ class NFSCluster:
     def _call_orch_apply_nfs(
             self,
             cluster_id: str,
-            placement: Optional[str],
+            placement: Optional[str] = None,
             virtual_ip: Optional[str] = None,
+            ingress_mode: Optional[IngressType] = None,
             port: Optional[int] = None,
     ) -> None:
         if not port:
@@ -69,18 +71,27 @@ class NFSCluster:
         if virtual_ip:
             # nfs + ingress
             # run NFS on non-standard port
+            if not ingress_mode:
+                ingress_mode = IngressType.default
+            pspec = PlacementSpec.from_string(placement)
+            if ingress_mode == IngressType.keepalive_only:
+                # enforce count=1 for nfs over keepalive only
+                pspec.count = 1
             spec = NFSServiceSpec(service_type='nfs', service_id=cluster_id,
-                                  placement=PlacementSpec.from_string(placement),
+                                  placement=pspec,
                                   # use non-default port so we don't conflict with ingress
-                                  port=10000 + port)   # semi-arbitrary, fix me someday
+                                  port=10000 + port if ingress_mode != IngressType.keepalive_only else port,  # semi-arbitrary, fix me someday
+                                  virtual_ip=virtual_ip.split('/')[0] if ingress_mode == IngressType.keepalive_only else None)
             completion = self.mgr.apply_nfs(spec)
             orchestrator.raise_if_exception(completion)
             ispec = IngressSpec(service_type='ingress',
                                 service_id='nfs.' + cluster_id,
                                 backend_service='nfs.' + cluster_id,
-                                frontend_port=port,
+                                placement=pspec,
+                                frontend_port=port if ingress_mode != IngressType.keepalive_only else None,
                                 monitor_port=7000 + port,   # semi-arbitrary, fix me someday
-                                virtual_ip=virtual_ip)
+                                virtual_ip=virtual_ip,
+                                keepalive_only=(ingress_mode == IngressType.keepalive_only))
             completion = self.mgr.apply_ingress(ispec)
             orchestrator.raise_if_exception(completion)
         else:
@@ -109,6 +120,7 @@ class NFSCluster:
             placement: Optional[str],
             virtual_ip: Optional[str],
             ingress: Optional[bool] = None,
+            ingress_mode: Optional[IngressType] = None,
             port: Optional[int] = None,
     ) -> None:
         try:
@@ -121,6 +133,8 @@ class NFSCluster:
                 raise NFSInvalidOperation('virtual_ip can only be provided with ingress enabled')
             if not virtual_ip and ingress:
                 raise NFSInvalidOperation('ingress currently requires a virtual_ip')
+            if ingress_mode and not ingress:
+                raise NFSInvalidOperation('--ingress-mode must be passed along with --ingress')
             invalid_str = re.search('[^A-Za-z0-9-_.]', cluster_id)
             if invalid_str:
                 raise NFSInvalidOperation(f"cluster id {cluster_id} is invalid. "
@@ -131,7 +145,7 @@ class NFSCluster:
             self.create_empty_rados_obj(cluster_id)
 
             if cluster_id not in available_clusters(self.mgr):
-                self._call_orch_apply_nfs(cluster_id, placement, virtual_ip, port)
+                self._call_orch_apply_nfs(cluster_id, placement, virtual_ip, ingress_mode, port)
                 return
             raise NonFatalError(f"{cluster_id} cluster already exists")
         except Exception as e:
index 2774f0945314fb26fe3d7cfa0525359354ada08a..a984500eebf114f0dfed4ce3501b73f4b67808bc 100644 (file)
@@ -5,6 +5,7 @@ from typing import Tuple, Optional, List, Dict, Any
 from mgr_module import MgrModule, CLICommand, Option, CLICheckNonemptyFileInput
 import object_format
 import orchestrator
+from orchestrator.module import IngressType
 
 from .export import ExportMgr, AppliedExportResults
 from .cluster import NFSCluster
@@ -121,11 +122,12 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
                                 placement: Optional[str] = None,
                                 ingress: Optional[bool] = None,
                                 virtual_ip: Optional[str] = None,
+                                ingress_mode: Optional[IngressType] = None,
                                 port: Optional[int] = None) -> None:
         """Create an NFS Cluster"""
         return self.nfs.create_nfs_cluster(cluster_id=cluster_id, placement=placement,
                                            virtual_ip=virtual_ip, ingress=ingress,
-                                           port=port)
+                                           ingress_mode=ingress_mode, port=port)
 
     @CLICommand('nfs cluster rm', perm='rw')
     @object_format.EmptyResponder()
index c32a6dfda53e53ac7947c339b7784209381ce071..7d8703bde7b4f565b58d2093790e46845da115d8 100644 (file)
@@ -170,6 +170,11 @@ class DaemonAction(enum.Enum):
     rotate_key = 'rotate-key'
 
 
+class IngressType(enum.Enum):
+    default = 'default'
+    keepalive_only = 'keepalive-only'
+
+
 def to_format(what: Any, format: Format, many: bool, cls: Any) -> Any:
     def to_json_1(obj: Any) -> Any:
         if hasattr(obj, 'to_json'):
index 9d78ce83ed8ff34b6b8d8362dc4156d50a78f096..7cf609384d49570992711ca74934ff77e98c4a9d 100644 (file)
@@ -810,6 +810,7 @@ class NFSServiceSpec(ServiceSpec):
                  config: Optional[Dict[str, str]] = None,
                  networks: Optional[List[str]] = None,
                  port: Optional[int] = None,
+                 virtual_ip: Optional[str] = None,
                  extra_container_args: Optional[List[str]] = None,
                  extra_entrypoint_args: Optional[List[str]] = None,
                  custom_configs: Optional[List[CustomConfig]] = None,
@@ -822,6 +823,7 @@ class NFSServiceSpec(ServiceSpec):
             extra_entrypoint_args=extra_entrypoint_args, custom_configs=custom_configs)
 
         self.port = port
+        self.virtual_ip = virtual_ip
 
     def get_port_start(self) -> List[int]:
         if self.port:
@@ -1051,6 +1053,7 @@ class IngressSpec(ServiceSpec):
                  virtual_interface_networks: Optional[List[str]] = [],
                  unmanaged: bool = False,
                  ssl: bool = False,
+                 keepalive_only: bool = False,
                  extra_container_args: Optional[List[str]] = None,
                  extra_entrypoint_args: Optional[List[str]] = None,
                  custom_configs: Optional[List[CustomConfig]] = None,
@@ -1081,10 +1084,15 @@ class IngressSpec(ServiceSpec):
         self.virtual_interface_networks = virtual_interface_networks or []
         self.unmanaged = unmanaged
         self.ssl = ssl
+        self.keepalive_only = keepalive_only
 
     def get_port_start(self) -> List[int]:
-        return [cast(int, self.frontend_port),
-                cast(int, self.monitor_port)]
+        ports = []
+        if self.frontend_port is not None:
+            ports.append(cast(int, self.frontend_port))
+        if self.monitor_port is not None:
+            ports.append(cast(int, self.monitor_port))
+        return ports
 
     def get_virtual_ip(self) -> Optional[str]:
         return self.virtual_ip
@@ -1095,7 +1103,7 @@ class IngressSpec(ServiceSpec):
         if not self.backend_service:
             raise SpecValidationError(
                 'Cannot add ingress: No backend_service specified')
-        if not self.frontend_port:
+        if not self.keepalive_only and not self.frontend_port:
             raise SpecValidationError(
                 'Cannot add ingress: No frontend_port specified')
         if not self.monitor_port: