Add argument 'mounts' to container

Introduce an option for users to bind-mount Cinder volumes to
containers. The syntax will look like:

  $ zun run --mount source=my-volume,destination=/data <image>

The above command will bind-mount cinder volume with name 'my-volume'
into path '/data' inside the container.

Change-Id: If8e44527e65cf574400b9043108e75b473b3627c
Partial-Implements: blueprint direct-cinder-integration
This commit is contained in:
Hongbin Lu 2017-08-06 22:36:32 +00:00
parent 3a3f4535b9
commit ec873e01a3
7 changed files with 97 additions and 6 deletions

View File

@ -30,7 +30,7 @@ if not LOG.handlers:
HEADER_NAME = "OpenStack-API-Version"
SERVICE_TYPE = "container"
DEFAULT_API_VERSION = '1.9'
DEFAULT_API_VERSION = '1.11'
_SUBSTITUTIONS = {}

View File

@ -198,6 +198,33 @@ def parse_command(command):
return " ".join(output)
def parse_mounts(mounts):
err_msg = ("Invalid mounts argument '%s'. mounts arguments must be of "
"the form --mount source=<volume>,destination=<path>.")
parsed_mounts = []
for mount in mounts:
mount_info = {"source": "", "destination": ""}
for mnt in mount.split(","):
try:
k, v = mnt.split("=", 1)
k = k.strip()
v = v.strip()
except ValueError:
raise apiexec.CommandError(err_msg % mnt)
if k in mount_info:
if mount_info[k]:
raise apiexec.CommandError(err_msg % mnt)
mount_info[k] = v
else:
raise apiexec.CommandError(err_msg % mnt)
if not mount_info['source'] or not mount_info['destination']:
raise apiexec.CommandError(err_msg % mnt)
parsed_mounts.append(mount_info)
return parsed_mounts
def parse_nets(ns):
err_msg = ("Invalid nets argument '%s'. nets arguments must be of "
"the form --nets <network=network, v4-fixed-ip=ip-addr,"

View File

@ -137,6 +137,13 @@ class CreateContainer(command.ShowOne):
' port: attach container to the neutron port with this UUID. '
'v4-fixed-ip: IPv4 fixed address for container. '
'v6-fixed-ip: IPv6 fixed address for container.')
parser.add_argument(
'--mount',
action='append',
default=[],
metavar='<mount>',
help='A dictionary to configure volumes mounted inside the '
'container.')
parser.add_argument(
'--rm',
dest='auto_remove',
@ -180,6 +187,7 @@ class CreateContainer(command.ShowOne):
opts['interactive'] = True
opts['hints'] = zun_utils.format_args(parsed_args.hint)
opts['nets'] = zun_utils.parse_nets(parsed_args.net)
opts['mounts'] = zun_utils.parse_mounts(parsed_args.mount)
opts['runtime'] = parsed_args.runtime
opts['hostname'] = parsed_args.hostname
@ -680,6 +688,13 @@ class RunContainer(command.ShowOne):
' port: attach container to the neutron port with this UUID. '
'v4-fixed-ip: IPv4 fixed address for container. '
'v6-fixed-ip: IPv6 fixed address for container.')
parser.add_argument(
'--mount',
action='append',
default=[],
metavar='<mount>',
help='A dictionary to configure volumes mounted inside the '
'container.')
parser.add_argument(
'--rm',
dest='auto_remove',
@ -723,6 +738,7 @@ class RunContainer(command.ShowOne):
opts['interactive'] = True
opts['hints'] = zun_utils.format_args(parsed_args.hint)
opts['nets'] = zun_utils.parse_nets(parsed_args.net)
opts['mounts'] = zun_utils.parse_mounts(parsed_args.mount)
opts['runtime'] = parsed_args.runtime
opts['hostname'] = parsed_args.hostname

View File

@ -246,7 +246,7 @@ class ShellTest(utils.TestCase):
project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None,
endpoint_override=None, insecure=False,
version=api_versions.APIVersion('1.9'))
version=api_versions.APIVersion('1.11'))
def test_main_option_region(self):
self.make_env()
@ -274,7 +274,7 @@ class ShellTest(utils.TestCase):
project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None,
endpoint_override=None, insecure=False,
version=api_versions.APIVersion('1.9'))
version=api_versions.APIVersion('1.11'))
@mock.patch('zunclient.client.Client')
def test_main_endpoint_internal(self, mock_client):
@ -288,7 +288,7 @@ class ShellTest(utils.TestCase):
project_domain_id='', project_domain_name='',
user_domain_id='', user_domain_name='', profile=None,
endpoint_override=None, insecure=False,
version=api_versions.APIVersion('1.9'))
version=api_versions.APIVersion('1.11'))
class ShellTestKeystoneV3(ShellTest):
@ -319,4 +319,4 @@ class ShellTestKeystoneV3(ShellTest):
project_domain_id='', project_domain_name='Default',
user_domain_id='', user_domain_name='Default',
endpoint_override=None, insecure=False, profile=None,
version=api_versions.APIVersion('1.9'))
version=api_versions.APIVersion('1.11'))

View File

@ -12,6 +12,7 @@
import mock
from zunclient.common.apiclient import exceptions as apiexec
from zunclient.common import utils as zun_utils
from zunclient.common.websocketclient import exceptions
from zunclient.tests.unit.v1 import shell_test_base
@ -167,6 +168,39 @@ class ShellTest(shell_test_base.TestCommandLineArgument):
self._invalid_choice_error)
self.assertFalse(mock_run.called)
@mock.patch('zunclient.v1.containers_shell._show_container')
@mock.patch('zunclient.v1.containers.ContainerManager.run')
def test_zun_container_run_with_mount(
self, mock_run, mock_show_container):
mock_run.return_value = 'container'
self._test_arg_success(
'run --mount source=s,destination=d x')
mock_show_container.assert_called_once_with('container')
def test_zun_container_run_with_mount_invalid_format(self):
self.assertRaisesRegex(
apiexec.CommandError, 'Invalid mounts argument',
self.shell,
'run --mount source,destination=d x')
def test_zun_container_run_with_mount_missed_key(self):
self.assertRaisesRegex(
apiexec.CommandError, 'Invalid mounts argument',
self.shell,
'run --mount source=s x')
def test_zun_container_run_with_mount_duplicated_key(self):
self.assertRaisesRegex(
apiexec.CommandError, 'Invalid mounts argument',
self.shell,
'run --mount source=s,source=s,destination=d x')
def test_zun_container_run_with_mount_invalid_key(self):
self.assertRaisesRegex(
apiexec.CommandError, 'Invalid mounts argument',
self.shell,
'run --mount invalid=s,destination=d x')
@mock.patch('zunclient.v1.containers_shell._show_container')
@mock.patch('zunclient.v1.containers.ContainerManager.run')
def test_zun_container_run_success_with_hostname(

View File

@ -23,7 +23,7 @@ CREATION_ATTRIBUTES = ['name', 'image', 'command', 'cpu', 'memory',
'environment', 'workdir', 'labels', 'image_pull_policy',
'restart_policy', 'interactive', 'image_driver',
'security_groups', 'hints', 'nets', 'auto_remove',
'runtime', 'hostname']
'runtime', 'hostname', 'mounts']
class Container(base.Resource):

View File

@ -112,6 +112,12 @@ def _show_container(container):
'port: attach container to the neutron port with this UUID. '
'v4-fixed-ip: IPv4 fixed address for container. '
'v6-fixed-ip: IPv6 fixed address for container.')
@utils.arg('--mount',
action='append',
default=[],
metavar='<mount>',
help='A dictionary to configure volumes mounted inside the '
'container.')
@utils.arg('--runtime',
metavar='<runtime>',
choices=['runc'],
@ -136,6 +142,7 @@ def do_create(cs, args):
opts['image_driver'] = args.image_driver
opts['hints'] = zun_utils.format_args(args.hint)
opts['nets'] = zun_utils.parse_nets(args.net)
opts['mounts'] = zun_utils.parse_mounts(args.mount)
opts['runtime'] = args.runtime
opts['hostname'] = args.hostname
@ -503,6 +510,12 @@ def do_kill(cs, args):
'port: attach container to the neutron port with this UUID. '
'v4-fixed-ip: IPv4 fixed address for container. '
'v6-fixed-ip: IPv6 fixed address for container.')
@utils.arg('--mount',
action='append',
default=[],
metavar='<mount>',
help='A dictionary to configure volumes mounted inside the '
'container.')
@utils.arg('--runtime',
metavar='<runtime>',
choices=['runc'],
@ -527,6 +540,7 @@ def do_run(cs, args):
opts['image_driver'] = args.image_driver
opts['hints'] = zun_utils.format_args(args.hint)
opts['nets'] = zun_utils.parse_nets(args.net)
opts['mounts'] = zun_utils.parse_mounts(args.mount)
opts['runtime'] = args.runtime
opts['hostname'] = args.hostname