# 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. import tempfile from oslo_concurrency import processutils as putils from oslo_log import log as logging from oslo_serialization import jsonutils as json from oslo_utils import excutils from oslo_utils import uuidutils import six from cinder import exception from cinder import utils from cinder.volume.targets import nvmeof LOG = logging.getLogger(__name__) class NVMET(nvmeof.NVMeOF): @utils.synchronized('nvmetcli', external=True) def create_nvmeof_target(self, volume_id, subsystem_name, target_ip, target_port, transport_type, nvmet_port_id, ns_id, volume_path): # Create NVME subsystem for previously created LV nvmf_subsystems = self._get_available_nvmf_subsystems() # Check if subsystem already exists search_for_subsystem = self._get_nvmf_subsystem( nvmf_subsystems, volume_id) if search_for_subsystem is None: newly_added_subsystem = self._add_nvmf_subsystem( nvmf_subsystems, target_ip, target_port, nvmet_port_id, subsystem_name, ns_id, volume_id, volume_path) if newly_added_subsystem is None: LOG.error('Failed to add subsystem: %s', subsystem_name) raise exception.NVMETTargetAddError(subsystem=subsystem_name) LOG.info('Added subsystem: %s', newly_added_subsystem) search_for_subsystem = newly_added_subsystem else: LOG.debug('Skip creating subsystem %s as ' 'it already exists.', search_for_subsystem) return { 'location': self.get_nvmeof_location( search_for_subsystem, target_ip, target_port, transport_type, ns_id), 'auth': ''} def _restore(self, nvmf_subsystems): # Dump updated JSON dict to append new subsystem with tempfile.NamedTemporaryFile() as tmp_fd: tmp_fd.write(json.dumps(nvmf_subsystems)) tmp_fd.flush() cmd = [ 'nvmetcli', 'restore', tmp_fd.name] try: out, err = utils.execute(*cmd, run_as_root=True) except putils.ProcessExecutionError: with excutils.save_and_reraise_exception(): LOG.exception('Error from nvmetcli restore') def _add_nvmf_subsystem(self, nvmf_subsystems, target_ip, target_port, nvmet_port_id, nvmet_subsystem_name, nvmet_ns_id, volume_id, volume_path): subsystem_name = self._get_target_info(nvmet_subsystem_name, volume_id) # Create JSON sections for the new subsystem to be created # Port section port_section = { "addr": { "adrfam": "ipv4", "traddr": target_ip, "treq": "not specified", "trsvcid": target_port, "trtype": "rdma" }, "portid": nvmet_port_id, "referrals": [], "subsystems": [subsystem_name] } nvmf_subsystems['ports'].append(port_section) # Subsystem section subsystem_section = { "allowed_hosts": [], "attr": { "allow_any_host": "1" }, "namespaces": [ { "device": { "nguid": six.text_type(uuidutils.generate_uuid()), "path": volume_path, }, "enable": 1, "nsid": nvmet_ns_id } ], "nqn": subsystem_name} nvmf_subsystems['subsystems'].append(subsystem_section) LOG.info( 'Trying to load the following subsystems: %s', nvmf_subsystems) self._restore(nvmf_subsystems) return subsystem_name @utils.synchronized('nvmetcli', external=True) def delete_nvmeof_target(self, volume): nvmf_subsystems = self._get_available_nvmf_subsystems() subsystem_name = self._get_nvmf_subsystem( nvmf_subsystems, volume['id']) if subsystem_name: removed_subsystem = self._delete_nvmf_subsystem( nvmf_subsystems, subsystem_name) if removed_subsystem is None: LOG.error( 'Failed to delete subsystem: %s', subsystem_name) raise exception.NVMETTargetDeleteError( subsystem=subsystem_name) elif removed_subsystem == subsystem_name: LOG.info( 'Managed to delete subsystem: %s', subsystem_name) return removed_subsystem else: LOG.info("Skipping remove_export. No NVMe subsystem " "for volume: %s", volume['id']) def _delete_nvmf_subsystem(self, nvmf_subsystems, subsystem_name): LOG.debug( 'Removing this subsystem: %s', subsystem_name) for port in nvmf_subsystems['ports']: if subsystem_name in port['subsystems']: port['subsystems'].remove(subsystem_name) break for subsys in nvmf_subsystems['subsystems']: if subsys['nqn'] == subsystem_name: nvmf_subsystems['subsystems'].remove(subsys) break LOG.debug( 'Newly loaded subsystems will be: %s', nvmf_subsystems) self._restore(nvmf_subsystems) return subsystem_name def _get_nvmf_subsystem(self, nvmf_subsystems, volume_id): subsystem_name = self._get_target_info( self.nvmet_subsystem_name, volume_id) for subsys in nvmf_subsystems['subsystems']: if subsys['nqn'] == subsystem_name: return subsystem_name def _get_available_nvmf_subsystems(self): __, tmp_file_path = tempfile.mkstemp(prefix='nvmet') # nvmetcli doesn't support printing to stdout yet, cmd = [ 'nvmetcli', 'save', tmp_file_path] try: out, err = utils.execute(*cmd, run_as_root=True) except putils.ProcessExecutionError: with excutils.save_and_reraise_exception(): LOG.exception('Error from nvmetcli save') self._delete_file(tmp_file_path) # temp file must be readable by this process user # in order to avoid executing cat as root with utils.temporary_chown(tmp_file_path): cmd = ['cat', tmp_file_path] try: out, err = utils.execute(*cmd) except putils.ProcessExecutionError: with excutils.save_and_reraise_exception(): LOG.exception('Failed to read: %s', tmp_file_path) self._delete_file(tmp_file_path) nvmf_subsystems = json.loads(out) self._delete_file(tmp_file_path) return nvmf_subsystems def _get_target_info(self, subsystem, volume_id): return "nqn.%s-%s" % (subsystem, volume_id) def _delete_file(self, file_path): cmd = ['rm', '-f', file_path] try: out, err = utils.execute(*cmd, run_as_root=True) except putils.ProcessExecutionError: LOG.exception('Failed to delete file: %s', file_path)