Coverage for nova/virt/libvirt/volume/remotefs.py: 84%

108 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:16 +0000

1# Copyright 2014 Cloudbase Solutions Srl 

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 

16import abc 

17import os 

18 

19from oslo_concurrency import processutils 

20from oslo_log import log as logging 

21from oslo_utils import fileutils 

22from oslo_utils import importutils 

23 

24import nova.conf 

25import nova.privsep.fs 

26from nova import utils 

27 

28LOG = logging.getLogger(__name__) 

29 

30CONF = nova.conf.CONF 

31 

32 

33def mount_share(mount_path, export_path, 

34 export_type, options=None): 

35 """Mount a remote export to mount_path. 

36 

37 :param mount_path: place where the remote export will be mounted 

38 :param export_path: path of the export to be mounted 

39 :export_type: remote export type (e.g. cifs, nfs, etc.) 

40 :options: A list containing mount options 

41 """ 

42 fileutils.ensure_tree(mount_path) 

43 

44 try: 

45 nova.privsep.fs.mount(export_type, export_path, mount_path, options) 

46 except processutils.ProcessExecutionError as exc: 

47 if 'Device or resource busy' in str(exc): 

48 LOG.warning("%s is already mounted", export_path) 

49 else: 

50 raise 

51 

52 

53def unmount_share(mount_path, export_path): 

54 """Unmount a remote share. 

55 

56 :param mount_path: remote export mount point 

57 :param export_path: path of the remote export to be unmounted 

58 """ 

59 try: 

60 nova.privsep.fs.umount(mount_path) 

61 except processutils.ProcessExecutionError as exc: 

62 if 'target is busy' in str(exc): 

63 LOG.debug("The share %s is still in use.", export_path) 

64 else: 

65 LOG.exception("Couldn't unmount the share %s", export_path) 

66 

67 

68class RemoteFilesystem(object): 

69 """Represents actions that can be taken on a remote host's filesystem.""" 

70 

71 def __init__(self): 

72 transport = CONF.libvirt.remote_filesystem_transport 

73 cls_name = '.'.join([__name__, transport.capitalize()]) 

74 cls_name += 'Driver' 

75 self.driver = importutils.import_object(cls_name) 

76 

77 def create_file(self, host, dst_path, on_execute=None, 

78 on_completion=None): 

79 LOG.debug("Creating file %s on remote host %s", dst_path, host) 

80 self.driver.create_file(host, dst_path, on_execute=on_execute, 

81 on_completion=on_completion) 

82 

83 def remove_file(self, host, dst_path, on_execute=None, 

84 on_completion=None): 

85 LOG.debug("Removing file %s on remote host %s", dst_path, host) 

86 self.driver.remove_file(host, dst_path, on_execute=on_execute, 

87 on_completion=on_completion) 

88 

89 def create_dir(self, host, dst_path, on_execute=None, 

90 on_completion=None): 

91 LOG.debug("Creating directory %s on remote host %s", dst_path, host) 

92 self.driver.create_dir(host, dst_path, on_execute=on_execute, 

93 on_completion=on_completion) 

94 

95 def remove_dir(self, host, dst_path, on_execute=None, 

96 on_completion=None): 

97 LOG.debug("Removing directory %s on remote host %s", dst_path, host) 

98 self.driver.remove_dir(host, dst_path, on_execute=on_execute, 

99 on_completion=on_completion) 

100 

101 def copy_file(self, src, dst, on_execute=None, 

102 on_completion=None, compression=True): 

103 LOG.debug("Copying file %s to %s", src, dst) 

104 self.driver.copy_file(src, dst, on_execute=on_execute, 

105 on_completion=on_completion, 

106 compression=compression) 

107 

108 

109class RemoteFilesystemDriver(metaclass=abc.ABCMeta): 

110 @abc.abstractmethod 

111 def create_file(self, host, dst_path, on_execute, on_completion): 

112 """Create file on the remote system. 

113 

114 :param host: Remote host 

115 :param dst_path: Destination path 

116 :param on_execute: Callback method to store pid of process in cache 

117 :param on_completion: Callback method to remove pid of process from 

118 cache 

119 """ 

120 

121 @abc.abstractmethod 

122 def remove_file(self, host, dst_path, on_execute, on_completion): 

123 """Removes a file on a remote host. 

124 

125 :param host: Remote host 

126 :param dst_path: Destination path 

127 :param on_execute: Callback method to store pid of process in cache 

128 :param on_completion: Callback method to remove pid of process from 

129 cache 

130 """ 

131 

132 @abc.abstractmethod 

133 def create_dir(self, host, dst_path, on_execute, on_completion): 

134 """Create directory on the remote system. 

135 

136 :param host: Remote host 

137 :param dst_path: Destination path 

138 :param on_execute: Callback method to store pid of process in cache 

139 :param on_completion: Callback method to remove pid of process from 

140 cache 

141 """ 

142 

143 @abc.abstractmethod 

144 def remove_dir(self, host, dst_path, on_execute, on_completion): 

145 """Removes a directory on a remote host. 

146 

147 :param host: Remote host 

148 :param dst_path: Destination path 

149 :param on_execute: Callback method to store pid of process in cache 

150 :param on_completion: Callback method to remove pid of process from 

151 cache 

152 """ 

153 

154 @abc.abstractmethod 

155 def copy_file(self, src, dst, on_execute, on_completion, compression): 

156 """Copy file to/from remote host. 

157 

158 Remote address must be specified in format: 

159 REM_HOST_IP_ADDRESS:REM_HOST_PATH 

160 For example: 

161 192.168.1.10:/home/file 

162 

163 :param src: Source address 

164 :param dst: Destination path 

165 :param on_execute: Callback method to store pid of process in cache 

166 :param on_completion: Callback method to remove pid of process from 

167 :param compression: If true, compress files when copying; drivers may 

168 ignore this if compression is not supported 

169 """ 

170 

171 

172class SshDriver(RemoteFilesystemDriver): 

173 

174 def create_file(self, host, dst_path, on_execute, on_completion): 

175 utils.ssh_execute(host, 'touch', dst_path, 

176 on_execute=on_execute, on_completion=on_completion) 

177 

178 def remove_file(self, host, dst, on_execute, on_completion): 

179 utils.ssh_execute(host, 'rm', dst, 

180 on_execute=on_execute, on_completion=on_completion) 

181 

182 def create_dir(self, host, dst_path, on_execute, on_completion): 

183 utils.ssh_execute(host, 'mkdir', '-p', dst_path, 

184 on_execute=on_execute, on_completion=on_completion) 

185 

186 def remove_dir(self, host, dst, on_execute, on_completion): 

187 utils.ssh_execute(host, 'rm', '-rf', dst, 

188 on_execute=on_execute, on_completion=on_completion) 

189 

190 def copy_file(self, src, dst, on_execute, on_completion, compression): 

191 args = ['scp'] 

192 if compression: 

193 args.append('-C') 

194 # As far as ploop disks are in fact directories we add '-r' argument 

195 args.extend(['-r', src, dst]) 

196 processutils.execute( 

197 *args, on_execute=on_execute, on_completion=on_completion) 

198 

199 

200class RsyncDriver(RemoteFilesystemDriver): 

201 

202 def create_file(self, host, dst_path, on_execute, on_completion): 

203 with utils.tempdir() as tempdir: 

204 dir_path = os.path.dirname(os.path.normpath(dst_path)) 

205 

206 # Create target dir inside temporary directory 

207 local_tmp_dir = os.path.join(tempdir, 

208 dir_path.strip(os.path.sep)) 

209 processutils.execute('mkdir', '-p', local_tmp_dir, 

210 on_execute=on_execute, 

211 on_completion=on_completion) 

212 

213 # Create file in directory 

214 file_name = os.path.basename(os.path.normpath(dst_path)) 

215 local_tmp_file = os.path.join(local_tmp_dir, file_name) 

216 processutils.execute('touch', local_tmp_file, 

217 on_execute=on_execute, 

218 on_completion=on_completion) 

219 RsyncDriver._synchronize_object(tempdir, 

220 host, dst_path, 

221 on_execute=on_execute, 

222 on_completion=on_completion) 

223 

224 def remove_file(self, host, dst, on_execute, on_completion): 

225 with utils.tempdir() as tempdir: 

226 RsyncDriver._remove_object(tempdir, host, dst, 

227 on_execute=on_execute, 

228 on_completion=on_completion) 

229 

230 def create_dir(self, host, dst_path, on_execute, on_completion): 

231 with utils.tempdir() as tempdir: 

232 dir_path = os.path.normpath(dst_path) 

233 

234 # Create target dir inside temporary directory 

235 local_tmp_dir = os.path.join(tempdir, 

236 dir_path.strip(os.path.sep)) 

237 processutils.execute('mkdir', '-p', local_tmp_dir, 

238 on_execute=on_execute, 

239 on_completion=on_completion) 

240 RsyncDriver._synchronize_object(tempdir, 

241 host, dst_path, 

242 on_execute=on_execute, 

243 on_completion=on_completion) 

244 

245 def remove_dir(self, host, dst, on_execute, on_completion): 

246 # Remove remote directory's content 

247 with utils.tempdir() as tempdir: 

248 processutils.execute('rsync', '--archive', '--delete-excluded', 

249 tempdir + os.path.sep, 

250 utils.format_remote_path(host, dst), 

251 on_execute=on_execute, 

252 on_completion=on_completion) 

253 

254 # Delete empty directory 

255 RsyncDriver._remove_object(tempdir, host, dst, 

256 on_execute=on_execute, 

257 on_completion=on_completion) 

258 

259 @staticmethod 

260 def _remove_object(src, host, dst, on_execute, on_completion): 

261 """Removes a file or empty directory on a remote host. 

262 

263 :param src: Empty directory used for rsync purposes 

264 :param host: Remote host 

265 :param dst: Destination path 

266 :param on_execute: Callback method to store pid of process in cache 

267 :param on_completion: Callback method to remove pid of process from 

268 cache 

269 """ 

270 processutils.execute( 

271 'rsync', '--archive', '--delete', 

272 '--include', os.path.basename(os.path.normpath(dst)), 

273 '--exclude', '*', 

274 os.path.normpath(src) + os.path.sep, 

275 utils.format_remote_path(host, 

276 os.path.dirname(os.path.normpath(dst))), 

277 on_execute=on_execute, on_completion=on_completion) 

278 

279 @staticmethod 

280 def _synchronize_object(src, host, dst, on_execute, on_completion): 

281 """Creates a file or empty directory on a remote host. 

282 

283 :param src: Empty directory used for rsync purposes 

284 :param host: Remote host 

285 :param dst: Destination path 

286 :param on_execute: Callback method to store pid of process in cache 

287 :param on_completion: Callback method to remove pid of process from 

288 cache 

289 """ 

290 

291 # For creating path on the remote host rsync --relative path must 

292 # be used. With a modern rsync on the sending side (beginning with 

293 # 2.6.7), you can insert a dot and a slash into the source path, 

294 # like this: 

295 # rsync -avR /foo/./bar/baz.c remote:/tmp/ 

296 # That would create /tmp/bar/baz.c on the remote machine. 

297 # (Note that the dot must be followed by a slash, so "/foo/." 

298 # would not be abbreviated.) 

299 relative_tmp_file_path = os.path.join( 

300 src, './', 

301 os.path.normpath(dst).strip(os.path.sep)) 

302 

303 # Do relative rsync local directory with remote root directory 

304 processutils.execute( 

305 'rsync', '--archive', '--relative', '--no-implied-dirs', 

306 relative_tmp_file_path, 

307 utils.format_remote_path(host, os.path.sep), 

308 on_execute=on_execute, on_completion=on_completion) 

309 

310 def copy_file(self, src, dst, on_execute, on_completion, compression): 

311 # As far as ploop disks are in fact directories we add '-r' argument 

312 args = ['rsync', '-r', '--sparse', src, dst] 

313 if compression: 

314 args.append('--compress') 

315 processutils.execute( 

316 *args, on_execute=on_execute, on_completion=on_completion)