Make block-device-mapping more stable and clear
The patch fix the following issues: 1. ValueError is raised if input don't contain "=". Sometimes the whole "server create" command is very complex, it's difficult to find out root reason directly. 2. Don't support to add block device from snapshot, like: --block-device-mapping vdb=0c8ae9d8-cadc-4a23-8337-4254614d277e:snapshot:1, it's supported by novaclient, but not in osc. 3. If input "vdb=", not add any mapping information, the server will be launched successfully, not raise error message to let use add volume/snapshot id, just ignore "--block-device-mapping" option. 4. The help message of "block-device-mapping" option is so simple, need to add some details about how to add <type>, <delete_on_terminate> contains. Change-Id: Ib7f7a654c3dc2a8272545f168b4c4ced230ce39e Depends-On: Ib37913891bbf7a31b570404c4668c490d5ac859b Closes-Bug: #1667266
This commit is contained in:
parent
0181de38af
commit
7a7bb06377
doc/source/command-objects
openstackclient
compute/v2
tests
releasenotes/notes
@ -193,7 +193,23 @@ Create a new server
|
|||||||
|
|
||||||
.. option:: --block-device-mapping <dev-name=mapping>
|
.. option:: --block-device-mapping <dev-name=mapping>
|
||||||
|
|
||||||
Map block devices; map is <id>:<type>:<size(GB)>:<delete_on_terminate> (optional extension)
|
Create a block device on the server.
|
||||||
|
|
||||||
|
Block device mapping in the format
|
||||||
|
|
||||||
|
<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>
|
||||||
|
|
||||||
|
<dev-name>: block device name, like: vdb, xvdc (required)
|
||||||
|
|
||||||
|
<id>: UUID of the volume or snapshot (required)
|
||||||
|
|
||||||
|
<type>: volume or snapshot; default: volume (optional)
|
||||||
|
|
||||||
|
<size(GB)>: volume size if create from snapshot (optional)
|
||||||
|
|
||||||
|
<delete-on-terminate>: true or false; default: false (optional)
|
||||||
|
|
||||||
|
(optional extension)
|
||||||
|
|
||||||
.. option:: --nic <net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,port-id=port-uuid,auto,none>
|
.. option:: --nic <net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,port-id=port-uuid,auto,none>
|
||||||
|
|
||||||
|
@ -446,10 +446,22 @@ class CreateServer(command.ShowOne):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--block-device-mapping',
|
'--block-device-mapping',
|
||||||
metavar='<dev-name=mapping>',
|
metavar='<dev-name=mapping>',
|
||||||
action='append',
|
action=parseractions.KeyValueAction,
|
||||||
default=[],
|
default={},
|
||||||
help=_('Map block devices; map is '
|
# NOTE(RuiChen): Add '\n' at the end of line to put each item in
|
||||||
'<id>:<type>:<size(GB)>:<delete_on_terminate> '
|
# the separated line, avoid the help message looks
|
||||||
|
# messy, see _SmartHelpFormatter in cliff.
|
||||||
|
help=_('Create a block device on the server.\n'
|
||||||
|
'Block device mapping in the format\n'
|
||||||
|
'<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>\n'
|
||||||
|
'<dev-name>: block device name, like: vdb, xvdc '
|
||||||
|
'(required)\n'
|
||||||
|
'<id>: UUID of the volume or snapshot (required)\n'
|
||||||
|
'<type>: volume or snapshot; default: volume (optional)\n'
|
||||||
|
'<size(GB)>: volume size if create from snapshot '
|
||||||
|
'(optional)\n'
|
||||||
|
'<delete-on-terminate>: true or false; default: false '
|
||||||
|
'(optional)\n'
|
||||||
'(optional extension)'),
|
'(optional extension)'),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -593,33 +605,39 @@ class CreateServer(command.ShowOne):
|
|||||||
'source_type': 'volume',
|
'source_type': 'volume',
|
||||||
'destination_type': 'volume'
|
'destination_type': 'volume'
|
||||||
}]
|
}]
|
||||||
for dev_map in parsed_args.block_device_mapping:
|
# Handle block device by device name order, like: vdb -> vdc -> vdd
|
||||||
dev_name, dev_map = dev_map.split('=', 1)
|
for dev_name in sorted(six.iterkeys(parsed_args.block_device_mapping)):
|
||||||
if dev_map:
|
dev_map = parsed_args.block_device_mapping[dev_name]
|
||||||
dev_map = dev_map.split(':')
|
dev_map = dev_map.split(':')
|
||||||
if len(dev_map) > 0:
|
if dev_map[0]:
|
||||||
mapping = {
|
mapping = {'device_name': dev_name}
|
||||||
'device_name': dev_name,
|
# 1. decide source and destination type
|
||||||
'uuid': utils.find_resource(
|
if (len(dev_map) > 1 and
|
||||||
volume_client.volumes,
|
dev_map[1] in ('volume', 'snapshot')):
|
||||||
dev_map[0],
|
mapping['source_type'] = dev_map[1]
|
||||||
).id}
|
|
||||||
# Block device mapping v1 compatibility
|
|
||||||
if len(dev_map) > 1 and \
|
|
||||||
dev_map[1] in ('volume', 'snapshot'):
|
|
||||||
mapping['source_type'] = dev_map[1]
|
|
||||||
else:
|
|
||||||
mapping['source_type'] = 'volume'
|
|
||||||
mapping['destination_type'] = 'volume'
|
|
||||||
if len(dev_map) > 2 and dev_map[2]:
|
|
||||||
mapping['volume_size'] = dev_map[2]
|
|
||||||
if len(dev_map) > 3:
|
|
||||||
mapping['delete_on_termination'] = dev_map[3]
|
|
||||||
else:
|
else:
|
||||||
msg = _("Volume name or ID must be specified if "
|
mapping['source_type'] = 'volume'
|
||||||
"--block-device-mapping is specified")
|
mapping['destination_type'] = 'volume'
|
||||||
raise exceptions.CommandError(msg)
|
# 2. check target exist, update target uuid according by
|
||||||
block_device_mapping_v2.append(mapping)
|
# source type
|
||||||
|
if mapping['source_type'] == 'volume':
|
||||||
|
volume_id = utils.find_resource(
|
||||||
|
volume_client.volumes, dev_map[0]).id
|
||||||
|
mapping['uuid'] = volume_id
|
||||||
|
elif mapping['source_type'] == 'snapshot':
|
||||||
|
snapshot_id = utils.find_resource(
|
||||||
|
volume_client.volume_snapshots, dev_map[0]).id
|
||||||
|
mapping['uuid'] = snapshot_id
|
||||||
|
# 3. append size and delete_on_termination if exist
|
||||||
|
if len(dev_map) > 2 and dev_map[2]:
|
||||||
|
mapping['volume_size'] = dev_map[2]
|
||||||
|
if len(dev_map) > 3 and dev_map[3]:
|
||||||
|
mapping['delete_on_termination'] = dev_map[3]
|
||||||
|
else:
|
||||||
|
msg = _("Volume or snapshot (name or ID) must be specified if "
|
||||||
|
"--block-device-mapping is specified")
|
||||||
|
raise exceptions.CommandError(msg)
|
||||||
|
block_device_mapping_v2.append(mapping)
|
||||||
|
|
||||||
nics = []
|
nics = []
|
||||||
auto_or_none = False
|
auto_or_none = False
|
||||||
|
@ -388,6 +388,113 @@ class ServerTests(common.ComputeTestCase):
|
|||||||
cmd_output['status'],
|
cmd_output['status'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_server_boot_with_bdm_snapshot(self):
|
||||||
|
"""Test server create from image with bdm snapshot, server delete"""
|
||||||
|
# get volume status wait function
|
||||||
|
volume_wait_for = test_volume.VolumeTests(
|
||||||
|
methodName='wait_for',
|
||||||
|
).wait_for
|
||||||
|
|
||||||
|
# create source empty volume
|
||||||
|
empty_volume_name = uuid.uuid4().hex
|
||||||
|
cmd_output = json.loads(self.openstack(
|
||||||
|
'volume create -f json ' +
|
||||||
|
'--size 1 ' +
|
||||||
|
empty_volume_name
|
||||||
|
))
|
||||||
|
self.assertIsNotNone(cmd_output["id"])
|
||||||
|
self.addCleanup(self.openstack,
|
||||||
|
'volume delete ' + empty_volume_name)
|
||||||
|
self.assertEqual(
|
||||||
|
empty_volume_name,
|
||||||
|
cmd_output['name'],
|
||||||
|
)
|
||||||
|
volume_wait_for("volume", empty_volume_name, "available")
|
||||||
|
|
||||||
|
# create snapshot of source empty volume
|
||||||
|
empty_snapshot_name = uuid.uuid4().hex
|
||||||
|
cmd_output = json.loads(self.openstack(
|
||||||
|
'volume snapshot create -f json ' +
|
||||||
|
'--volume ' + empty_volume_name + ' ' +
|
||||||
|
empty_snapshot_name
|
||||||
|
))
|
||||||
|
self.assertIsNotNone(cmd_output["id"])
|
||||||
|
self.assertEqual(
|
||||||
|
empty_snapshot_name,
|
||||||
|
cmd_output['name'],
|
||||||
|
)
|
||||||
|
volume_wait_for("volume snapshot", empty_snapshot_name, "available")
|
||||||
|
|
||||||
|
# create server with bdm snapshot
|
||||||
|
server_name = uuid.uuid4().hex
|
||||||
|
server = json.loads(self.openstack(
|
||||||
|
'server create -f json ' +
|
||||||
|
'--flavor ' + self.flavor_name + ' ' +
|
||||||
|
'--image ' + self.image_name + ' ' +
|
||||||
|
'--block-device-mapping '
|
||||||
|
'vdb=' + empty_snapshot_name + ':snapshot:1:true ' +
|
||||||
|
self.network_arg + ' ' +
|
||||||
|
'--wait ' +
|
||||||
|
server_name
|
||||||
|
))
|
||||||
|
self.assertIsNotNone(server["id"])
|
||||||
|
self.assertEqual(
|
||||||
|
server_name,
|
||||||
|
server['name'],
|
||||||
|
)
|
||||||
|
self.wait_for_status(server_name, 'ACTIVE')
|
||||||
|
|
||||||
|
# check server volumes_attached, format is
|
||||||
|
# {"volumes_attached": "id='2518bc76-bf0b-476e-ad6b-571973745bb5'",}
|
||||||
|
cmd_output = json.loads(self.openstack(
|
||||||
|
'server show -f json ' +
|
||||||
|
server_name
|
||||||
|
))
|
||||||
|
volumes_attached = cmd_output['volumes_attached']
|
||||||
|
self.assertTrue(volumes_attached.startswith('id='))
|
||||||
|
attached_volume_id = volumes_attached.replace('id=', '')
|
||||||
|
|
||||||
|
# check the volume that attached on server
|
||||||
|
cmd_output = json.loads(self.openstack(
|
||||||
|
'volume show -f json ' +
|
||||||
|
attached_volume_id
|
||||||
|
))
|
||||||
|
attachments = cmd_output['attachments']
|
||||||
|
self.assertEqual(
|
||||||
|
1,
|
||||||
|
len(attachments),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
server['id'],
|
||||||
|
attachments[0]['server_id'],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"in-use",
|
||||||
|
cmd_output['status'],
|
||||||
|
)
|
||||||
|
|
||||||
|
# delete server, then check the attached volume had been deleted,
|
||||||
|
# <delete-on-terminate>=true
|
||||||
|
self.openstack('server delete --wait ' + server_name)
|
||||||
|
cmd_output = json.loads(self.openstack(
|
||||||
|
'volume list -f json'
|
||||||
|
))
|
||||||
|
target_volume = [each_volume
|
||||||
|
for each_volume in cmd_output
|
||||||
|
if each_volume['ID'] == attached_volume_id]
|
||||||
|
if target_volume:
|
||||||
|
# check the attached volume is 'deleting' status
|
||||||
|
self.assertEqual('deleting', target_volume[0]['Status'])
|
||||||
|
else:
|
||||||
|
# the attached volume had been deleted
|
||||||
|
pass
|
||||||
|
|
||||||
|
# clean up volume snapshot manually, make sure the snapshot and volume
|
||||||
|
# can be deleted sequentially, self.addCleanup so fast, that cause
|
||||||
|
# volume service API 400 error and the volume is left over at the end.
|
||||||
|
self.openstack('volume snapshot delete ' + empty_snapshot_name)
|
||||||
|
volume_wait_for('volume snapshot', empty_snapshot_name, 'disappear')
|
||||||
|
|
||||||
def test_server_create_with_none_network(self):
|
def test_server_create_with_none_network(self):
|
||||||
"""Test server create with none network option."""
|
"""Test server create with none network option."""
|
||||||
server_name = uuid.uuid4().hex
|
server_name = uuid.uuid4().hex
|
||||||
|
@ -14,6 +14,8 @@ import json
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from tempest.lib import exceptions
|
||||||
|
|
||||||
from openstackclient.tests.functional.volume.v2 import common
|
from openstackclient.tests.functional.volume.v2 import common
|
||||||
|
|
||||||
|
|
||||||
@ -253,7 +255,13 @@ class VolumeTests(common.BaseVolumeTests):
|
|||||||
total_sleep = 0
|
total_sleep = 0
|
||||||
opts = self.get_opts(['status'])
|
opts = self.get_opts(['status'])
|
||||||
while total_sleep < wait:
|
while total_sleep < wait:
|
||||||
status = self.openstack(check_type + ' show ' + check_name + opts)
|
try:
|
||||||
|
status = self.openstack(
|
||||||
|
check_type + ' show ' + check_name + opts
|
||||||
|
)
|
||||||
|
except exceptions.CommandFailed:
|
||||||
|
# Show command raise Exception when object had been deleted
|
||||||
|
status = 'disappear'
|
||||||
status = status.rstrip()
|
status = status.rstrip()
|
||||||
print('Checking {} {} Waiting for {} current status: {}'
|
print('Checking {} {} Waiting for {} current status: {}'
|
||||||
.format(check_type, check_name, desired_status, status))
|
.format(check_type, check_name, desired_status, status))
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
|
import argparse
|
||||||
import collections
|
import collections
|
||||||
import getpass
|
import getpass
|
||||||
import mock
|
import mock
|
||||||
@ -49,6 +50,10 @@ class TestServer(compute_fakes.TestComputev2):
|
|||||||
self.volumes_mock = self.app.client_manager.volume.volumes
|
self.volumes_mock = self.app.client_manager.volume.volumes
|
||||||
self.volumes_mock.reset_mock()
|
self.volumes_mock.reset_mock()
|
||||||
|
|
||||||
|
# Get a shortcut to the volume client VolumeManager Mock
|
||||||
|
self.snapshots_mock = self.app.client_manager.volume.volume_snapshots
|
||||||
|
self.snapshots_mock.reset_mock()
|
||||||
|
|
||||||
# Set object attributes to be tested. Could be overwritten in subclass.
|
# Set object attributes to be tested. Could be overwritten in subclass.
|
||||||
self.attrs = {}
|
self.attrs = {}
|
||||||
|
|
||||||
@ -326,7 +331,9 @@ class TestServerCreate(TestServer):
|
|||||||
|
|
||||||
self.volume = volume_fakes.FakeVolume.create_one_volume()
|
self.volume = volume_fakes.FakeVolume.create_one_volume()
|
||||||
self.volumes_mock.get.return_value = self.volume
|
self.volumes_mock.get.return_value = self.volume
|
||||||
self.block_device_mapping = 'vda=' + self.volume.name + ':::0'
|
|
||||||
|
self.snapshot = volume_fakes.FakeSnapshot.create_one_snapshot()
|
||||||
|
self.snapshots_mock.get.return_value = self.snapshot
|
||||||
|
|
||||||
# Get the command object to test
|
# Get the command object to test
|
||||||
self.cmd = server.CreateServer(self.app, None)
|
self.cmd = server.CreateServer(self.app, None)
|
||||||
@ -852,13 +859,13 @@ class TestServerCreate(TestServer):
|
|||||||
arglist = [
|
arglist = [
|
||||||
'--image', 'image1',
|
'--image', 'image1',
|
||||||
'--flavor', self.flavor.id,
|
'--flavor', self.flavor.id,
|
||||||
'--block-device-mapping', self.block_device_mapping,
|
'--block-device-mapping', 'vda=' + self.volume.name + ':::false',
|
||||||
self.new_server.name,
|
self.new_server.name,
|
||||||
]
|
]
|
||||||
verifylist = [
|
verifylist = [
|
||||||
('image', 'image1'),
|
('image', 'image1'),
|
||||||
('flavor', self.flavor.id),
|
('flavor', self.flavor.id),
|
||||||
('block_device_mapping', [self.block_device_mapping]),
|
('block_device_mapping', {'vda': self.volume.name + ':::false'}),
|
||||||
('config_drive', False),
|
('config_drive', False),
|
||||||
('server_name', self.new_server.name),
|
('server_name', self.new_server.name),
|
||||||
]
|
]
|
||||||
@ -867,11 +874,6 @@ class TestServerCreate(TestServer):
|
|||||||
# CreateServer.take_action() returns two tuples
|
# CreateServer.take_action() returns two tuples
|
||||||
columns, data = self.cmd.take_action(parsed_args)
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
real_volume_mapping = (
|
|
||||||
(self.block_device_mapping.split('=', 1)[1]).replace(
|
|
||||||
self.volume.name,
|
|
||||||
self.volume.id))
|
|
||||||
|
|
||||||
# Set expected values
|
# Set expected values
|
||||||
kwargs = dict(
|
kwargs = dict(
|
||||||
meta=None,
|
meta=None,
|
||||||
@ -885,10 +887,10 @@ class TestServerCreate(TestServer):
|
|||||||
availability_zone=None,
|
availability_zone=None,
|
||||||
block_device_mapping_v2=[{
|
block_device_mapping_v2=[{
|
||||||
'device_name': 'vda',
|
'device_name': 'vda',
|
||||||
'uuid': real_volume_mapping.split(':', 1)[0],
|
'uuid': self.volume.id,
|
||||||
'destination_type': 'volume',
|
'destination_type': 'volume',
|
||||||
'source_type': 'volume',
|
'source_type': 'volume',
|
||||||
'delete_on_termination': '0'
|
'delete_on_termination': 'false',
|
||||||
}],
|
}],
|
||||||
nics=[],
|
nics=[],
|
||||||
scheduler_hints={},
|
scheduler_hints={},
|
||||||
@ -905,6 +907,323 @@ class TestServerCreate(TestServer):
|
|||||||
self.assertEqual(self.columns, columns)
|
self.assertEqual(self.columns, columns)
|
||||||
self.assertEqual(self.datalist(), data)
|
self.assertEqual(self.datalist(), data)
|
||||||
|
|
||||||
|
def test_server_create_with_block_device_mapping_min_input(self):
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping', 'vdf=' + self.volume.name,
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('image', 'image1'),
|
||||||
|
('flavor', self.flavor.id),
|
||||||
|
('block_device_mapping', {'vdf': self.volume.name}),
|
||||||
|
('config_drive', False),
|
||||||
|
('server_name', self.new_server.name),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
# CreateServer.take_action() returns two tuples
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
# Set expected values
|
||||||
|
kwargs = dict(
|
||||||
|
meta=None,
|
||||||
|
files={},
|
||||||
|
reservation_id=None,
|
||||||
|
min_count=1,
|
||||||
|
max_count=1,
|
||||||
|
security_groups=[],
|
||||||
|
userdata=None,
|
||||||
|
key_name=None,
|
||||||
|
availability_zone=None,
|
||||||
|
block_device_mapping_v2=[{
|
||||||
|
'device_name': 'vdf',
|
||||||
|
'uuid': self.volume.id,
|
||||||
|
'destination_type': 'volume',
|
||||||
|
'source_type': 'volume',
|
||||||
|
}],
|
||||||
|
nics=[],
|
||||||
|
scheduler_hints={},
|
||||||
|
config_drive=None,
|
||||||
|
)
|
||||||
|
# ServerManager.create(name, image, flavor, **kwargs)
|
||||||
|
self.servers_mock.create.assert_called_with(
|
||||||
|
self.new_server.name,
|
||||||
|
self.image,
|
||||||
|
self.flavor,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.columns, columns)
|
||||||
|
self.assertEqual(self.datalist(), data)
|
||||||
|
|
||||||
|
def test_server_create_with_block_device_mapping_default_input(self):
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping', 'vdf=' + self.volume.name + ':::',
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('image', 'image1'),
|
||||||
|
('flavor', self.flavor.id),
|
||||||
|
('block_device_mapping', {'vdf': self.volume.name + ':::'}),
|
||||||
|
('config_drive', False),
|
||||||
|
('server_name', self.new_server.name),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
# CreateServer.take_action() returns two tuples
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
# Set expected values
|
||||||
|
kwargs = dict(
|
||||||
|
meta=None,
|
||||||
|
files={},
|
||||||
|
reservation_id=None,
|
||||||
|
min_count=1,
|
||||||
|
max_count=1,
|
||||||
|
security_groups=[],
|
||||||
|
userdata=None,
|
||||||
|
key_name=None,
|
||||||
|
availability_zone=None,
|
||||||
|
block_device_mapping_v2=[{
|
||||||
|
'device_name': 'vdf',
|
||||||
|
'uuid': self.volume.id,
|
||||||
|
'destination_type': 'volume',
|
||||||
|
'source_type': 'volume',
|
||||||
|
}],
|
||||||
|
nics=[],
|
||||||
|
scheduler_hints={},
|
||||||
|
config_drive=None,
|
||||||
|
)
|
||||||
|
# ServerManager.create(name, image, flavor, **kwargs)
|
||||||
|
self.servers_mock.create.assert_called_with(
|
||||||
|
self.new_server.name,
|
||||||
|
self.image,
|
||||||
|
self.flavor,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.columns, columns)
|
||||||
|
self.assertEqual(self.datalist(), data)
|
||||||
|
|
||||||
|
def test_server_create_with_block_device_mapping_full_input(self):
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping',
|
||||||
|
'vde=' + self.volume.name + ':volume:3:true',
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('image', 'image1'),
|
||||||
|
('flavor', self.flavor.id),
|
||||||
|
('block_device_mapping',
|
||||||
|
{'vde': self.volume.name + ':volume:3:true'}),
|
||||||
|
('config_drive', False),
|
||||||
|
('server_name', self.new_server.name),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
# CreateServer.take_action() returns two tuples
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
# Set expected values
|
||||||
|
kwargs = dict(
|
||||||
|
meta=None,
|
||||||
|
files={},
|
||||||
|
reservation_id=None,
|
||||||
|
min_count=1,
|
||||||
|
max_count=1,
|
||||||
|
security_groups=[],
|
||||||
|
userdata=None,
|
||||||
|
key_name=None,
|
||||||
|
availability_zone=None,
|
||||||
|
block_device_mapping_v2=[{
|
||||||
|
'device_name': 'vde',
|
||||||
|
'uuid': self.volume.id,
|
||||||
|
'destination_type': 'volume',
|
||||||
|
'source_type': 'volume',
|
||||||
|
'delete_on_termination': 'true',
|
||||||
|
'volume_size': '3'
|
||||||
|
}],
|
||||||
|
nics=[],
|
||||||
|
scheduler_hints={},
|
||||||
|
config_drive=None,
|
||||||
|
)
|
||||||
|
# ServerManager.create(name, image, flavor, **kwargs)
|
||||||
|
self.servers_mock.create.assert_called_with(
|
||||||
|
self.new_server.name,
|
||||||
|
self.image,
|
||||||
|
self.flavor,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.columns, columns)
|
||||||
|
self.assertEqual(self.datalist(), data)
|
||||||
|
|
||||||
|
def test_server_create_with_block_device_mapping_snapshot(self):
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping',
|
||||||
|
'vds=' + self.volume.name + ':snapshot:5:true',
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('image', 'image1'),
|
||||||
|
('flavor', self.flavor.id),
|
||||||
|
('block_device_mapping',
|
||||||
|
{'vds': self.volume.name + ':snapshot:5:true'}),
|
||||||
|
('config_drive', False),
|
||||||
|
('server_name', self.new_server.name),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
# CreateServer.take_action() returns two tuples
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
# Set expected values
|
||||||
|
kwargs = dict(
|
||||||
|
meta=None,
|
||||||
|
files={},
|
||||||
|
reservation_id=None,
|
||||||
|
min_count=1,
|
||||||
|
max_count=1,
|
||||||
|
security_groups=[],
|
||||||
|
userdata=None,
|
||||||
|
key_name=None,
|
||||||
|
availability_zone=None,
|
||||||
|
block_device_mapping_v2=[{
|
||||||
|
'device_name': 'vds',
|
||||||
|
'uuid': self.snapshot.id,
|
||||||
|
'destination_type': 'volume',
|
||||||
|
'source_type': 'snapshot',
|
||||||
|
'delete_on_termination': 'true',
|
||||||
|
'volume_size': '5'
|
||||||
|
}],
|
||||||
|
nics=[],
|
||||||
|
scheduler_hints={},
|
||||||
|
config_drive=None,
|
||||||
|
)
|
||||||
|
# ServerManager.create(name, image, flavor, **kwargs)
|
||||||
|
self.servers_mock.create.assert_called_with(
|
||||||
|
self.new_server.name,
|
||||||
|
self.image,
|
||||||
|
self.flavor,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.columns, columns)
|
||||||
|
self.assertEqual(self.datalist(), data)
|
||||||
|
|
||||||
|
def test_server_create_with_block_device_mapping_multiple(self):
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping', 'vdb=' + self.volume.name + ':::false',
|
||||||
|
'--block-device-mapping', 'vdc=' + self.volume.name + ':::true',
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('image', 'image1'),
|
||||||
|
('flavor', self.flavor.id),
|
||||||
|
('block_device_mapping', {'vdb': self.volume.name + ':::false',
|
||||||
|
'vdc': self.volume.name + ':::true'}),
|
||||||
|
('config_drive', False),
|
||||||
|
('server_name', self.new_server.name),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
# CreateServer.take_action() returns two tuples
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
# Set expected values
|
||||||
|
kwargs = dict(
|
||||||
|
meta=None,
|
||||||
|
files={},
|
||||||
|
reservation_id=None,
|
||||||
|
min_count=1,
|
||||||
|
max_count=1,
|
||||||
|
security_groups=[],
|
||||||
|
userdata=None,
|
||||||
|
key_name=None,
|
||||||
|
availability_zone=None,
|
||||||
|
block_device_mapping_v2=[
|
||||||
|
{
|
||||||
|
'device_name': 'vdb',
|
||||||
|
'uuid': self.volume.id,
|
||||||
|
'destination_type': 'volume',
|
||||||
|
'source_type': 'volume',
|
||||||
|
'delete_on_termination': 'false',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'device_name': 'vdc',
|
||||||
|
'uuid': self.volume.id,
|
||||||
|
'destination_type': 'volume',
|
||||||
|
'source_type': 'volume',
|
||||||
|
'delete_on_termination': 'true',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nics=[],
|
||||||
|
scheduler_hints={},
|
||||||
|
config_drive=None,
|
||||||
|
)
|
||||||
|
# ServerManager.create(name, image, flavor, **kwargs)
|
||||||
|
self.servers_mock.create.assert_called_with(
|
||||||
|
self.new_server.name,
|
||||||
|
self.image,
|
||||||
|
self.flavor,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.columns, columns)
|
||||||
|
self.assertEqual(self.datalist(), data)
|
||||||
|
|
||||||
|
def test_server_create_with_block_device_mapping_invalid_format(self):
|
||||||
|
# 1. block device mapping don't contain equal sign "="
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping', 'not_contain_equal_sign',
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
self.assertRaises(argparse.ArgumentTypeError,
|
||||||
|
self.check_parser,
|
||||||
|
self.cmd, arglist, [])
|
||||||
|
# 2. block device mapping don't contain device name "=uuid:::true"
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping', '=uuid:::true',
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
self.assertRaises(argparse.ArgumentTypeError,
|
||||||
|
self.check_parser,
|
||||||
|
self.cmd, arglist, [])
|
||||||
|
|
||||||
|
def test_server_create_with_block_device_mapping_no_uuid(self):
|
||||||
|
arglist = [
|
||||||
|
'--image', 'image1',
|
||||||
|
'--flavor', self.flavor.id,
|
||||||
|
'--block-device-mapping', 'vdb=',
|
||||||
|
self.new_server.name,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('image', 'image1'),
|
||||||
|
('flavor', self.flavor.id),
|
||||||
|
('block_device_mapping', {'vdb': ''}),
|
||||||
|
('config_drive', False),
|
||||||
|
('server_name', self.new_server.name),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.CommandError,
|
||||||
|
self.cmd.take_action,
|
||||||
|
parsed_args)
|
||||||
|
|
||||||
|
|
||||||
class TestServerDelete(TestServer):
|
class TestServerDelete(TestServer):
|
||||||
|
|
||||||
|
9
releasenotes/notes/bug-1667266-6497727abc2af9a5.yaml
Normal file
9
releasenotes/notes/bug-1667266-6497727abc2af9a5.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Make ``block-device-mapping`` option of ``server create`` command more
|
||||||
|
stable and clear. Fix ValueError when input block device mapping option in
|
||||||
|
wrong format. Support to create block device from snapshot. Add details in
|
||||||
|
help message about block-device-mapping option format and regular value of
|
||||||
|
each item.
|
||||||
|
[Bug `1667266 <https://bugs.launchpad.net/python-openstackclient/+bug/1667266>`_]
|
Loading…
x
Reference in New Issue
Block a user