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
« 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.
16import re
17import uuid
19from oslo_log import log
20from oslo_serialization import jsonutils
21from oslo_utils import excutils
23from manila import exception
24from manila.i18n import _
25from manila.share import driver
28LOG = log.getLogger(__name__)
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()
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)
74 self.disconnect_network("bridge", name)
75 LOG.info("A container has been successfully created! Its id is %s.",
76 result[0].rstrip("\n"))
78 def start_container(self, name):
79 cmd = ["docker", "container", "start", name]
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)
87 LOG.info("Container %s successfully started!", name)
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)
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
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
121 def fetch_container_addresses(self, name, address_family="inet6"):
122 addresses = []
123 interfaces = self.fetch_container_interfaces(name)
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])
135 return addresses
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")]
143 for link in links:
144 interface = re.search(" (.+?)@", link).group(1)
145 interfaces.append(interface)
147 return interfaces
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)
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)
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)
176 LOG.info("Container %s has been successfully renamed.", name)
178 def container_exists(self, name):
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
187 def create_network(self, network_name):
188 cmd = ["docker", "network", "create", network_name]
189 LOG.debug("Creating the %s Docker network.", network_name)
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)
197 LOG.info("The Docker network has been successfully created! Its id is "
198 "%s.", result[0].rstrip("\n"))
200 def remove_network(self, network_name):
201 cmd = ["docker", "network", "remove", network_name]
202 LOG.debug("Removing the %s Docker network.", network_name)
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)
211 LOG.info("The %s Docker network has been successfully removed!",
212 result[0].rstrip("\n"))
214 def connect_network(self, network_name, container_name):
215 cmd = ["docker", "network", "connect", network_name, container_name]
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))
224 LOG.info("Docker network %s has been successfully connected to "
225 "container %s!", network_name, container_name)
227 def disconnect_network(self, network_name, container_name):
228 cmd = ["docker", "network", "disconnect", network_name, container_name]
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))
237 LOG.debug("Docker network %s has been successfully disconnected from "
238 "container %s!", network_name, container_name)
240 def get_container_networks(self, container_name):
241 cmd = ["docker", "container", "inspect", "-f",
242 "'{{json .NetworkSettings.Networks}}'", container_name]
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)
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
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)
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)
267 return veths
269 def get_network_bridge(self, network_name):
270 cmd = ["docker", "network", "inspect", "-f", "{{.Id}}", network_name]
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)
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]
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