1# Copyright (c) 2013 OpenStack Foundation 2# All Rights Reserved 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15 16"""Remote filesystem client utilities.""" 17 18import os 19import re 20import tempfile 21 22from oslo_concurrency import processutils 23from oslo_log import log as logging 24from oslo_utils.secretutils import md5 25 26from os_brick import exception 27from os_brick import executor 28from os_brick.i18n import _ 29 30LOG = logging.getLogger(__name__) 31 32 33class RemoteFsClient(executor.Executor): 34 35 def __init__(self, mount_type, root_helper, 36 execute=None, *args, **kwargs): 37 super(RemoteFsClient, self).__init__(root_helper, execute=execute, 38 *args, **kwargs) # type: ignore 39 40 mount_type_to_option_prefix = { 41 'nfs': 'nfs', 42 'cifs': 'smbfs', 43 'glusterfs': 'glusterfs', 44 'vzstorage': 'vzstorage', 45 'quobyte': 'quobyte', 46 'scality': 'scality' 47 } 48 49 if mount_type not in mount_type_to_option_prefix: 50 raise exception.ProtocolNotSupported(protocol=mount_type) 51 52 self._mount_type = mount_type 53 option_prefix = mount_type_to_option_prefix[mount_type] 54 55 self._mount_base: str 56 self._mount_base = kwargs.get(option_prefix + 57 '_mount_point_base') # type: ignore 58 if not self._mount_base: 59 raise exception.InvalidParameterValue( 60 err=_('%s_mount_point_base required') % option_prefix) 61 62 self._mount_options = kwargs.get(option_prefix + '_mount_options') 63 64 if mount_type == "nfs": 65 self._check_nfs_options() 66 67 def get_mount_base(self): 68 return self._mount_base 69 70 def _get_hash_str(self, base_str): 71 """Return a string that represents hash of base_str (hex format).""" 72 if isinstance(base_str, str): 73 base_str = base_str.encode('utf-8') 74 return md5(base_str, 75 usedforsecurity=False).hexdigest() 76 77 def get_mount_point(self, device_name: str): 78 """Get Mount Point. 79 80 :param device_name: example 172.18.194.100:/var/nfs 81 """ 82 return os.path.join(self._mount_base, 83 self._get_hash_str(device_name)) 84 85 def _read_mounts(self): 86 """Returns a dict of mounts and their mountpoint 87 88 Format reference: 89 http://man7.org/linux/man-pages/man5/fstab.5.html 90 """ 91 with open("/proc/mounts", "r") as mounts: 92 # Remove empty lines and split lines by whitespace 93 lines = [line.split() for line in mounts.read().splitlines() 94 if line.strip()] 95 96 # Return {mountpoint: mountdevice}. Fields 2nd and 1st as per 97 # http://man7.org/linux/man-pages/man5/fstab.5.html 98 return {line[1]: line[0] for line in lines if line[0] != '#'} 99 100 def mount(self, share, flags=None): 101 """Mount given share.""" 102 mount_path = self.get_mount_point(share) 103 104 if mount_path in self._read_mounts(): 105 LOG.debug('Already mounted: %s', mount_path) 106 return 107 108 self._execute('mkdir', '-p', mount_path, check_exit_code=0) 109 if self._mount_type == 'nfs': 110 self._mount_nfs(share, mount_path, flags) 111 else: 112 self._do_mount(self._mount_type, share, mount_path, 113 self._mount_options, flags) 114 115 def _do_mount(self, mount_type, share, mount_path, mount_options=None, 116 flags=None): 117 """Mounts share based on the specified params.""" 118 mnt_cmd = ['mount', '-t', mount_type] 119 if mount_options is not None: 120 mnt_cmd.extend(['-o', mount_options]) 121 if flags is not None: 122 mnt_cmd.extend(flags) 123 mnt_cmd.extend([share, mount_path]) 124 125 try: 126 self._execute(*mnt_cmd, root_helper=self._root_helper, 127 run_as_root=True, check_exit_code=0) 128 except processutils.ProcessExecutionError as exc: 129 if 'already mounted' in exc.stderr: 130 LOG.debug("Already mounted: %s", share) 131 132 # The error message can say "busy or already mounted" when the 133 # share didn't actually mount, so look for it. 134 if share in self._read_mounts(): 135 return 136 137 LOG.error("Failed to mount %(share)s, reason: %(reason)s", 138 {'share': share, 'reason': exc.stderr}) 139 raise 140 141 def _mount_nfs(self, nfs_share, mount_path, flags=None): 142 """Mount nfs share using present mount types.""" 143 mnt_errors = {} 144 145 # This loop allows us to first try to mount with NFS 4.1 for pNFS 146 # support but falls back to mount NFS 4 or NFS 3 if either the client 147 # or server do not support it. 148 for mnt_type in sorted(self._nfs_mount_type_opts.keys(), reverse=True): 149 options = self._nfs_mount_type_opts[mnt_type] 150 try: 151 self._do_mount('nfs', nfs_share, mount_path, options, flags) 152 LOG.debug('Mounted %(sh)s using %(mnt_type)s.', 153 {'sh': nfs_share, 'mnt_type': mnt_type}) 154 return 155 except Exception as e: 156 mnt_errors[mnt_type] = str(e) 157 LOG.debug('Failed to do %s mount.', mnt_type) 158 raise exception.BrickException(_("NFS mount failed for share %(sh)s. " 159 "Error - %(error)s") 160 % {'sh': nfs_share, 161 'error': mnt_errors}) 162 163 def _check_nfs_options(self): 164 """Checks and prepares nfs mount type options.""" 165 self._nfs_mount_type_opts = {'nfs': self._mount_options} 166 nfs_vers_opt_patterns = ['^nfsvers', '^vers', r'^v[\d]'] 167 for opt in nfs_vers_opt_patterns: 168 if self._option_exists(self._mount_options, opt): 169 return 170 171 # pNFS requires NFS 4.1. The mount.nfs4 utility does not automatically 172 # negotiate 4.1 support, we have to ask for it by specifying two 173 # options: vers=4 and minorversion=1. 174 pnfs_opts = self._update_option(self._mount_options, 'vers', '4') 175 pnfs_opts = self._update_option(pnfs_opts, 'minorversion', '1') 176 self._nfs_mount_type_opts['pnfs'] = pnfs_opts 177 178 def _option_exists(self, options, opt_pattern): 179 """Checks if the option exists in nfs options and returns position.""" 180 options = [x.strip() for x in options.split(',')] if options else [] 181 pos = 0 182 for opt in options: 183 pos = pos + 1 184 if re.match(opt_pattern, opt, flags=0): 185 return pos 186 return 0 187 188 def _update_option(self, options, option, value=None): 189 """Update option if exists else adds it and returns new options.""" 190 opts = [x.strip() for x in options.split(',')] if options else [] 191 pos = self._option_exists(options, option) 192 if pos: 193 opts.pop(pos - 1) 194 opt = '%s=%s' % (option, value) if value else option 195 opts.append(opt) 196 return ",".join(opts) if len(opts) > 1 else opts[0] 197 198 199class ScalityRemoteFsClient(RemoteFsClient): 200 def __init__(self, mount_type, root_helper, 201 execute=None, *args, **kwargs): 202 super(ScalityRemoteFsClient, self).__init__( 203 mount_type, root_helper, execute=execute, 204 *args, **kwargs) # type: ignore 205 self._mount_type = mount_type 206 self._mount_base = kwargs.get( 207 'scality_mount_point_base', "").rstrip('/') 208 if not self._mount_base: 209 raise exception.InvalidParameterValue( 210 err=_('scality_mount_point_base required')) 211 self._mount_options = None 212 213 def get_mount_point(self, device_name): 214 return os.path.join(self._mount_base, 215 device_name, 216 "00") 217 218 def mount(self, share, flags=None): 219 """Mount the Scality ScaleOut FS. 220 221 The `share` argument is ignored because you can't mount several 222 SOFS at the same type on a single server. But we want to keep the 223 same method signature for class inheritance purpose. 224 """ 225 if self._mount_base in self._read_mounts(): 226 LOG.debug('Already mounted: %s', self._mount_base) 227 return 228 self._execute('mkdir', '-p', self._mount_base, check_exit_code=0) 229 super(ScalityRemoteFsClient, self)._do_mount( 230 'sofs', '/usr/local/etc/sfused.conf', self._mount_base) 231 232 233class VZStorageRemoteFSClient(RemoteFsClient): 234 def _vzstorage_write_mds_list(self, cluster_name, mdss): 235 tmp_dir = tempfile.mkdtemp(prefix='vzstorage-') 236 tmp_bs_path = os.path.join(tmp_dir, 'bs_list') 237 with open(tmp_bs_path, 'w') as f: 238 for mds in mdss: 239 f.write(mds + "\n") 240 241 conf_dir = os.path.join('/usr/local/etc/pstorage/clusters', cluster_name) 242 if os.path.exists(conf_dir): 243 bs_path = os.path.join(conf_dir, 'bs_list') 244 self._execute('cp', '-f', tmp_bs_path, bs_path, 245 root_helper=self._root_helper, run_as_root=True) 246 else: 247 self._execute('cp', '-rf', tmp_dir, conf_dir, 248 root_helper=self._root_helper, run_as_root=True) 249 self._execute('chown', '-R', 'root:root', conf_dir, 250 root_helper=self._root_helper, run_as_root=True) 251 252 def _do_mount(self, mount_type, vz_share, mount_path, 253 mount_options=None, flags=None): 254 m = re.search(r"(?:(\S+):\/)?([a-zA-Z0-9_-]+)(?::(\S+))?", vz_share) 255 if not m: 256 msg = (_("Invalid Virtuozzo Storage share specification: %r." 257 "Must be: [MDS1[,MDS2],...:/]<CLUSTER NAME>[:PASSWORD].") 258 % vz_share) 259 raise exception.BrickException(msg) 260 261 mdss = m.group(1) 262 cluster_name = m.group(2) 263 passwd = m.group(3) 264 265 if mdss: 266 mdss = mdss.split(',') 267 self._vzstorage_write_mds_list(cluster_name, mdss) 268 269 if passwd: 270 self._execute('pstorage', '-c', cluster_name, 'auth-node', '-P', 271 process_input=passwd, 272 root_helper=self._root_helper, run_as_root=True) 273 274 mnt_cmd = ['pstorage-mount', '-c', cluster_name] 275 if flags: 276 mnt_cmd.extend(flags) 277 mnt_cmd.extend([mount_path]) 278 279 self._execute(*mnt_cmd, root_helper=self._root_helper, 280 run_as_root=True, check_exit_code=0) 281