Coverage for manila/share/drivers/container/container_helper.py: 100%

159 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright (c) 2016 Mirantis, Inc. 

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 re 

17import uuid 

18 

19from oslo_log import log 

20from oslo_serialization import jsonutils 

21from oslo_utils import excutils 

22 

23from manila import exception 

24from manila.i18n import _ 

25from manila.share import driver 

26 

27 

28LOG = log.getLogger(__name__) 

29 

30 

31class DockerExecHelper(driver.ExecuteMixin): 

32 def __init__(self, *args, **kwargs): 

33 self.configuration = kwargs.pop("configuration", None) 

34 super(DockerExecHelper, self).__init__(*args, **kwargs) 

35 self.init_execute_mixin() 

36 

37 def create_container(self, name=None): 

38 name = name or "".join(["manila_cifs_docker_container", 

39 str(uuid.uuid1()).replace("-", "_")]) 

40 image_name = self.configuration.container_image_name 

41 LOG.debug("Starting container from image %s.", image_name) 

42 # (aovchinnikov): --privileged is required for both samba and 

43 # nfs-ganesha to actually allow access to shared folders. 

44 # 

45 # (aovchinnikov): To actually make docker container mount a 

46 # logical volume created after container start-up to some location 

47 # inside it, we must share entire /dev with it. While seemingly 

48 # dangerous it is not and moreover this is apparently the only sane 

49 # way to do it. The reason is when a logical volume gets created 

50 # several new things appear in /dev: a new /dev/dm-X and a symlink 

51 # in /dev/volume_group_name pointing to /dev/dm-X. But to be able 

52 # to interact with /dev/dm-X, it must be already present inside 

53 # the container's /dev i.e. it must have been -v shared during 

54 # container start-up. So we should either precreate an unknown 

55 # number of /dev/dm-Xs (one per LV), share them all and hope 

56 # for the best or share the entire /dev and hope for the best. 

57 # 

58 # The risk of allowing a container having access to entire host's 

59 # /dev is not as big as it seems: as long as actual share providers 

60 # are invulnerable this does not pose any extra risks. If, however, 

61 # share providers contain vulnerabilities then the driver does not 

62 # provide any more possibilities for an exploitation than other 

63 # first-party drivers. 

64 path = "{0}:/shares".format( 

65 self.configuration.container_volume_mount_path) 

66 cmd = ["docker", "container", "create", "--name=%s" % name, 

67 "--privileged", "-v", "/dev:/dev", "-v", path, image_name] 

68 try: 

69 result = self._inner_execute(cmd) 

70 except (exception.ProcessExecutionError, OSError): 

71 raise exception.ShareBackendException( 

72 msg="Container %s failed to be created." % name) 

73 

74 self.disconnect_network("bridge", name) 

75 LOG.info("A container has been successfully created! Its id is %s.", 

76 result[0].rstrip("\n")) 

77 

78 def start_container(self, name): 

79 cmd = ["docker", "container", "start", name] 

80 

81 try: 

82 self._inner_execute(cmd) 

83 except (exception.ProcessExecutionError, OSError): 

84 raise exception.ShareBackendException( 

85 msg="Container %s has failed to start." % name) 

86 

87 LOG.info("Container %s successfully started!", name) 

88 

89 def stop_container(self, name): 

90 LOG.debug("Stopping container %s.", name) 

91 try: 

92 self._inner_execute(["docker", "stop", name]) 

93 except (exception.ProcessExecutionError, OSError): 

94 raise exception.ShareBackendException( 

95 msg="Container %s has failed to stop properly." % name) 

96 LOG.info("Container %s is successfully stopped.", name) 

97 

98 def execute(self, name=None, cmd=None, ignore_errors=False): 

99 if name is None: 

100 raise exception.ManilaException(_("Container name not specified.")) 

101 if cmd is None or (type(cmd) is not list): 

102 raise exception.ManilaException(_("Missing or malformed command.")) 

103 LOG.debug("Executing inside a container %s.", name) 

104 cmd = ["docker", "exec", "-i", name] + cmd 

105 result = self._inner_execute(cmd, ignore_errors=ignore_errors) 

106 return result 

107 

108 def _inner_execute(self, cmd, ignore_errors=False): 

109 LOG.debug("Executing command: %s.", " ".join(cmd)) 

110 try: 

111 result = self._execute(*cmd, run_as_root=True) 

112 except (exception.ProcessExecutionError, OSError) as e: 

113 with excutils.save_and_reraise_exception( 

114 reraise=not ignore_errors): 

115 LOG.warning("Failed to run command %(cmd)s due to " 

116 "%(reason)s.", {'cmd': cmd, 'reason': e}) 

117 else: 

118 LOG.debug("Execution result: %s.", result) 

119 return result 

120 

121 def fetch_container_addresses(self, name, address_family="inet6"): 

122 addresses = [] 

123 interfaces = self.fetch_container_interfaces(name) 

124 

125 for interface in interfaces: 

126 result = self.execute( 

127 name, 

128 ["ip", "-oneline", 

129 "-family", address_family, 

130 "address", "show", "scope", "global", "dev", interface], 

131 ) 

132 address_w_prefix = result[0].split()[3] 

133 addresses.append(address_w_prefix.split("/")[0]) 

134 

135 return addresses 

136 

137 def fetch_container_interfaces(self, name): 

138 interfaces = [] 

139 links = self.execute(name, ["ip", "-o", "link", "show"]) 

140 links = links[0].rstrip().split("\n") 

141 links = [link for link in links if link.split()[1].startswith("eth")] 

142 

143 for link in links: 

144 interface = re.search(" (.+?)@", link).group(1) 

145 interfaces.append(interface) 

146 

147 return interfaces 

148 

149 def rename_container(self, name, new_name): 

150 veth_names = self.get_container_veths(name) 

151 if not veth_names: 

152 raise exception.ManilaException( 

153 _("Could not find OVS information related to " 

154 "container %s.") % name) 

155 

156 try: 

157 self._inner_execute(["docker", "rename", name, new_name]) 

158 except (exception.ProcessExecutionError, OSError): 

159 raise exception.ShareBackendException( 

160 msg="Could not rename container %s." % name) 

161 

162 for veth_name in veth_names: 

163 cmd = ["ovs-vsctl", "set", "interface", veth_name, 

164 "external-ids:manila-container=%s" % new_name] 

165 try: 

166 self._inner_execute(cmd) 

167 except (exception.ProcessExecutionError, OSError): 

168 try: 

169 self._inner_execute(["docker", "rename", new_name, name]) 

170 except (exception.ProcessExecutionError, OSError): 

171 msg = _("Could not rename back container %s.") % name 

172 LOG.exception(msg) 

173 raise exception.ShareBackendException( 

174 msg="Could not update OVS information %s." % name) 

175 

176 LOG.info("Container %s has been successfully renamed.", name) 

177 

178 def container_exists(self, name): 

179 

180 result = self._execute("docker", "ps", "--no-trunc", 

181 "--format='{{.Names}}'", run_as_root=True)[0] 

182 for line in result.split('\n'): 

183 if name == line.strip("'"): 

184 return True 

185 return False 

186 

187 def create_network(self, network_name): 

188 cmd = ["docker", "network", "create", network_name] 

189 LOG.debug("Creating the %s Docker network.", network_name) 

190 

191 try: 

192 result = self._inner_execute(cmd) 

193 except (exception.ProcessExecutionError, OSError): 

194 raise exception.ShareBackendException( 

195 msg="Docker network %s could not be created." % network_name) 

196 

197 LOG.info("The Docker network has been successfully created! Its id is " 

198 "%s.", result[0].rstrip("\n")) 

199 

200 def remove_network(self, network_name): 

201 cmd = ["docker", "network", "remove", network_name] 

202 LOG.debug("Removing the %s Docker network.", network_name) 

203 

204 try: 

205 result = self._inner_execute(cmd) 

206 except (exception.ProcessExecutionError, OSError): 

207 raise exception.ShareBackendException( 

208 msg="Docker network %s could not be removed. One or more " 

209 "containers are probably still using it." % network_name) 

210 

211 LOG.info("The %s Docker network has been successfully removed!", 

212 result[0].rstrip("\n")) 

213 

214 def connect_network(self, network_name, container_name): 

215 cmd = ["docker", "network", "connect", network_name, container_name] 

216 

217 try: 

218 self._inner_execute(cmd) 

219 except (exception.ProcessExecutionError, OSError): 

220 raise exception.ShareBackendException( 

221 msg="Could not connect the Docker network %s to container %s." 

222 % (network_name, container_name)) 

223 

224 LOG.info("Docker network %s has been successfully connected to " 

225 "container %s!", network_name, container_name) 

226 

227 def disconnect_network(self, network_name, container_name): 

228 cmd = ["docker", "network", "disconnect", network_name, container_name] 

229 

230 try: 

231 self._inner_execute(cmd) 

232 except (exception.ProcessExecutionError, OSError): 

233 raise exception.ShareBackendException( 

234 msg="Could not disconnect the Docker network %s from " 

235 "container %s." % (network_name, container_name)) 

236 

237 LOG.debug("Docker network %s has been successfully disconnected from " 

238 "container %s!", network_name, container_name) 

239 

240 def get_container_networks(self, container_name): 

241 cmd = ["docker", "container", "inspect", "-f", 

242 "'{{json .NetworkSettings.Networks}}'", container_name] 

243 

244 try: 

245 result = self._inner_execute(cmd) 

246 except (exception.ProcessExecutionError, OSError): 

247 raise exception.ShareBackendException( 

248 msg="Could not find any networks associated with the %s " 

249 "container." % container_name) 

250 

251 # NOTE(ecsantos): The stdout from _inner_execute comes with extra 

252 # single quotes. 

253 networks = list(jsonutils.loads(result[0].strip("\n'"))) 

254 return networks 

255 

256 def get_container_veths(self, container_name): 

257 veths = [] 

258 cmd = ["bash", "-c", "cat /sys/class/net/eth*/iflink"] 

259 eths_iflinks = self.execute(container_name, cmd) 

260 

261 for eth_iflink in eths_iflinks[0].rstrip().split("\n"): 

262 veth = self._execute("bash", "-c", "grep -l %s " 

263 "/sys/class/net/veth*/ifindex" % eth_iflink) 

264 veth = re.search("t/(.+?)/i", veth[0]).group(1) 

265 veths.append(veth) 

266 

267 return veths 

268 

269 def get_network_bridge(self, network_name): 

270 cmd = ["docker", "network", "inspect", "-f", "{{.Id}}", network_name] 

271 

272 try: 

273 network_id = self._inner_execute(cmd) 

274 except (exception.ProcessExecutionError, OSError): 

275 raise exception.ShareBackendException( 

276 msg="Could not find the ID of the %s Docker network." 

277 % network_name) 

278 

279 # The name of the bridge associated with a given Docker network is 

280 # always "br-" followed by the first 12 digits of that network's ID. 

281 return "br-" + network_id[0][0:12] 

282 

283 def get_veth_from_bridge(self, bridge): 

284 veth = self._execute("ip", "link", "show", "master", bridge) 

285 veth = re.search(" (.+?)@", veth[0]).group(1) 

286 return veth