from ..security import Scope
from ..services.nvmeof_cli import NvmeofCLICommand, convert_to_bytes, \
escape_address_if_ipv6, format_host_updates
+from ..services.nvmeof_client import get_gateway_locations
from ..services.orchestrator import OrchClient
from ..tools import str_to_bool
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
@ReadPermission
@Endpoint('GET')
def group(self):
+ # pylint: disable=too-many-nested-blocks
try:
orch = OrchClient.instance()
- return orch.services.list(service_type='nvmeof')
+ services_result = orch.services.list(service_type='nvmeof')
+
+ if isinstance(services_result, tuple) and len(services_result) == 2:
+ services, count = services_result
+ else:
+ services = services_result
+ count = len(services) if services else 0
+
+ if services:
+ result = []
+ for service in services:
+ service_dict = service.to_json() if hasattr(service, 'to_json') else service
+
+ if isinstance(service_dict, dict):
+ # Extract pool and group from spec
+ spec = service_dict.get('spec', {})
+ if isinstance(spec, dict):
+ pool = spec.get('pool')
+ group = spec.get('group')
+
+ if pool and group:
+ # Get hosts list from placement to match location order
+ placement = service_dict.get('placement', {})
+ hosts = placement.get('hosts', [])
+
+ # Get locations in the same order as hosts
+ locations = get_gateway_locations(pool, group, hosts)
+
+ if 'placement' not in service_dict:
+ service_dict['placement'] = {}
+
+ service_dict['placement']['locations'] = locations
+
+ result.append(service_dict)
+
+ return (result, count)
+
+ return services_result
except OrchestratorError as e:
# just return none instead of raising an exception
# since we need this to work regardless of the status
# of orchestrator in UI
logger.error('Failed to fetch the gateway groups: %s', e)
return None
+ except Exception as e: # pylint: disable=broad-except
+ logger.error("Unexpected error in group(): %s", e, exc_info=True)
+ return None
@ReadPermission
@Endpoint('GET', '/version')
),
"disable_auto_resize": Param(str, "Disable auto resize", True, None),
"read_only": Param(str, "Read only namespace", True, None),
+ "location": Param(str, "Gateway location for namespace", True, None),
"gw_group": Param(str, "NVMeoF gateway group", True, None),
"server_address": Param(str, "NVMeoF gateway address", True, None),
},
no_auto_visible: Optional[bool] = False,
disable_auto_resize: Optional[bool] = False,
read_only: Optional[bool] = False,
+ location: Optional[str] = None,
gw_group: Optional[str] = None,
server_address: Optional[str] = None,
rados_namespace: Optional[str] = None,
force=force,
no_auto_visible=no_auto_visible,
disable_auto_resize=disable_auto_resize,
- read_only=read_only
+ read_only=read_only,
+ location=location
)
)
"load_balancing_group": Param(int, "Load balancing group"),
"disable_auto_resize": Param(str, "Disable auto resize", True, None),
"read_only": Param(str, "Read only namespace", True, None),
+ "location": Param(str, "Gateway location for namespace", True, None),
"force": Param(
bool,
"Force create namespace even it image is used by other namespace"
no_auto_visible: Optional[bool] = False,
disable_auto_resize: Optional[bool] = False,
read_only: Optional[bool] = False,
+ location: Optional[str] = None,
gw_group: Optional[str] = None,
server_address: Optional[str] = None,
rados_namespace: Optional[str] = None,
force=force,
no_auto_visible=no_auto_visible,
disable_auto_resize=disable_auto_resize,
- read_only=read_only
+ read_only=read_only,
+ location=location
)
)
"r_mbytes_per_second": Param(int, "Read MB/s"),
"w_mbytes_per_second": Param(int, "Write MB/s"),
"trash_image": Param(bool, "Trash RBD image after removing namespace"),
+ "location": Param(str, "Namespace location"),
"gw_group": Param(str, "NVMeoF gateway group", True, None),
"server_address": Param(str, "NVMeoF gateway address", True, None),
},
r_mbytes_per_second: Optional[int] = None,
w_mbytes_per_second: Optional[int] = None,
trash_image: Optional[bool] = None,
+ location: Optional[str] = None,
gw_group: Optional[str] = None,
server_address: Optional[str] = None,
):
if resp.status != 0:
contains_failure = True
+ if location is not None:
+ resp = NVMeoFClient(
+ gw_group=gw_group,
+ server_address=server_address
+ ).stub.namespace_change_location(
+ NVMeoFClient.pb2.namespace_change_location_req(
+ subsystem_nqn=nqn,
+ nsid=int(nsid),
+ location=location
+ )
+ )
+ if resp.status != 0:
+ contains_failure = True
+
if contains_failure:
cherrypy.response.status = 202
NamedTuple, Optional, Type, get_args, get_origin
from ..exceptions import DashboardException
+from ..services.ceph_service import CephService
from .nvmeof_conf import NvmeofGatewaysConfig, is_mtls_enabled
logger = logging.getLogger("nvmeof_client")
return field_to_ret
return wrapper
return decorator
+
+
+def get_gateway_locations(pool: str, group: str, hosts: Optional[List[str]] = None):
+ """
+ Get locations for gateways in a service group using nvme-gw show command.
+
+ Args:
+ pool: The RBD pool name
+ group: The NVMeoF gateway group name
+ hosts: Optional list of hostnames to match locations to (in order)
+
+ Returns:
+ If hosts provided: List of location strings matching the order of hosts
+ If hosts not provided: List of unique location strings (sorted)
+ """
+ try: # pylint: disable=too-many-nested-blocks
+ if not pool or not group:
+ logger.warning('Pool or group not provided for location lookup: pool=%s, group=%s',
+ pool, group)
+ return []
+
+ result = CephService.send_command('mon', 'nvme-gw show',
+ pool=pool, group=group)
+
+ # Build a mapping of hostname to location
+ host_to_location = {}
+ if isinstance(result, dict):
+ gateways = result.get('Created Gateways:', [])
+
+ for gw in gateways:
+ if isinstance(gw, dict):
+ gw_id = gw.get('gw-id', '')
+ location = gw.get('location', '')
+
+ # Extract hostname from gw-id (format: client.nvmeof.pool.group.hostname.xxx)
+ if gw_id:
+ parts = gw_id.split('.')
+ if len(parts) >= 5:
+ hostname = parts[4]
+ if location:
+ host_to_location[hostname] = location
+ else:
+ # If format is unexpected, log warning
+ logger.warning('Unexpected gateway ID format: %s', gw_id)
+
+ # If hosts list provided, return locations in the same order
+ if hosts:
+ locations = []
+ for host in hosts:
+ # Get location for this host, empty string if not found
+ location = host_to_location.get(host, '')
+ if not location:
+ logger.debug('No location found for host: %s', host)
+ locations.append(location)
+ return locations
+
+ # Otherwise return unique sorted locations
+ unique_locations = set(host_to_location.values())
+ return sorted(list(unique_locations))
+
+ except Exception as e: # pylint: disable=broad-except
+ logger.error('Failed to get gateway locations for pool=%s, group=%s: %s',
+ pool, group, e)
+ return []