cinder/cinder/privsep/targets/nvmet.py

242 lines
7.9 KiB
Python

# Copyright 2022 Red Hat, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""NVMet Python Interface using privsep
This file adds the privsep support to the nvmet package so it can be easily
consumed by Cinder nvmet target.
It also:
- Adds some methods to the Root class to be able to get a specific subsystem or
port directly without having to go through all the existing ones.
- Presents the CFSNotFound exception as a NotFound exception which is easier to
consume.
"""
import os
import nvmet
from oslo_log import log as logging
from cinder import exception
from cinder import privsep
LOG = logging.getLogger(__name__)
###################
# Helper methods to serialize/deserialize parameters to be sent through privsep
# and to do the instance/class calls on the privsep side.
def serialize(instance):
"""Serialize parameters, specially nvmet instances.
The idea is to be able to pass an nvmet instance to privsep methods, since
they are sometimes required as parameters (ie: port.setup) and also to pass
the instance where do_privsep_call has to call a specific method.
Instances are passed as a tuple, with the name of the class as the first
element, and in the second element the kwargs necessary to instantiate the
instance of that class.
To differentiate nvmet instances from tuples there is a 'tuple' value that
can be passed in the first element of the tuple to differentiate them.
All other instances as passed unaltered.
"""
if isinstance(instance, nvmet.Root):
return ('Root', {})
if isinstance(instance, (nvmet.Subsystem, nvmet.Host)):
return (type(instance).__name__, {'nqn': instance.nqn,
'mode': 'lookup'})
if isinstance(instance, nvmet.Namespace):
return ('Namespace', {'nsid': instance.nsid,
'subsystem': serialize(instance.subsystem),
'mode': 'lookup'})
if isinstance(instance, nvmet.Port):
return ('Port', {'portid': instance.portid, 'mode': 'lookup'})
if isinstance(instance, nvmet.Referral):
return ('Referral', {'name': instance.name,
'port': serialize(instance.port),
'mode': 'lookup'})
if isinstance(instance, nvmet.ANAGroup):
return ('ANAGroup', {'grpid': instance.grpid,
'port': serialize(instance.port),
'mode': 'lookup'})
if isinstance(instance, tuple):
return ('tuple', instance)
return instance
def deserialize(data):
"""Deserialize an instance, specially nvmet instances.
Reverse operation of the serialize method. Converts an nvmet instance
serialized in a tuple into an actual nvmet instance.
"""
if not isinstance(data, tuple):
return data
cls_name, cls_params = data
if cls_name == 'tuple':
return cls_params
# Parameters for the instantiation of the class can be nvmet objects
# themselves.
params = {name: deserialize(value) for name, value in cls_params.items()}
# We don't want the classes from the nvmet method but ours instead
instance = getattr(nvmet, cls_name)(**params)
return instance
def deserialize_params(args, kwargs):
"""Deserialize function arguments using deserialize method."""
args = [deserialize(arg) for arg in args]
kwargs = {key: deserialize(value) for key, value in kwargs.items()}
return args, kwargs
def _nvmet_setup_failure(message):
"""Simple error method to use when calling nvmet setup methods."""
LOG.error(message)
raise exception.CinderException(message)
@privsep.sys_admin_pctxt.entrypoint
def do_privsep_call(instance, method_name, *args, **kwargs):
"""General privsep method for instance calls.
Handle privsep method calls by deserializing the instance where we want to
call a given method with the deserialized parameters.
"""
LOG.debug('Calling %s on %s with %s - %s',
method_name, instance, args, kwargs)
instance = deserialize(instance)
method = getattr(instance, method_name)
args, kwargs = deserialize_params(args, kwargs)
# NOTE: No returning nvmet objects support. If needed add serialization on
# the result and deserialization decorator before the entrypoint.
return method(*args, **kwargs)
@privsep.sys_admin_pctxt.entrypoint
def _privsep_setup(cls_name, *args, **kwargs):
"""Special privsep method for nvmet setup method calls.
The setup method is a special case because it's a class method (which
privsep cannot handle) and also requires a function for the error handling.
This method accepts a class name and reconstructs it, then calls the
class' setup method passing our own error function.
"""
LOG.debug('Setup %s with %s - %s', cls_name, args, kwargs)
cls = getattr(nvmet, cls_name)
args, kwargs = deserialize_params(args, kwargs)
kwargs['err_func'] = _nvmet_setup_failure
return cls.setup(*args, **kwargs)
def privsep_setup(cls_name, *args, **kwargs):
"""Wrapper for _privsep_setup that accepts err_func argument."""
# err_func parameter hardcoded in _privsep_setup as it cannot be serialized
if 'err_func' in kwargs:
err_func = kwargs.pop('err_func')
else: # positional is always last argument of the args tuple
err_func = args[-1]
args = args[:-1]
try:
return _privsep_setup(cls_name, *args, **kwargs)
except exception.CinderException as exc:
if not err_func:
raise
err_func(exc.msg)
###################
# Classes that don't currently have privsep support
Host = nvmet.Host
Referral = nvmet.Referral
ANAGroup = nvmet.ANAGroup
###################
# Custom classes that divert privileges calls to privsep
# Support in these classes is limited to what's needed by the nvmet target.
# Convenience error class link to nvmet's
NotFound = nvmet.nvme.CFSNotFound
class Namespace(nvmet.Namespace):
def __init__(self, subsystem, nsid=None, mode='lookup'):
super().__init__(subsystem=subsystem, nsid=nsid, mode=mode)
@classmethod
def setup(cls, subsys, n, err_func=None):
privsep_setup(cls.__name__, serialize(subsys), n, err_func)
def delete(self):
do_privsep_call(serialize(self), 'delete')
class Subsystem(nvmet.Subsystem):
def __init__(self, nqn=None, mode='lookup'):
super().__init__(nqn=nqn, mode=mode)
@classmethod
def setup(cls, t, err_func=None):
privsep_setup(cls.__name__, t, err_func)
def delete(self):
do_privsep_call(serialize(self), 'delete')
@property
def namespaces(self):
for d in os.listdir(self.path + '/namespaces/'):
yield Namespace(self, os.path.basename(d))
class Port(nvmet.Port):
def __init__(self, portid, mode='lookup'):
super().__init__(portid=portid, mode=mode)
@classmethod
def setup(cls, root, n, err_func=None):
privsep_setup(cls.__name__, serialize(root), n, err_func)
def add_subsystem(self, nqn):
do_privsep_call(serialize(self), 'add_subsystem', nqn)
def remove_subsystem(self, nqn):
do_privsep_call(serialize(self), 'remove_subsystem', nqn)
def delete(self):
do_privsep_call(serialize(self), 'delete')
class Root(nvmet.Root):
@property
def ports(self):
for d in os.listdir(self.path + '/ports/'):
yield Port(os.path.basename(d))