Merge "compute: Add support for loading BDMs from files"

This commit is contained in:
Zuul 2021-03-11 12:51:47 +00:00 committed by Gerrit Code Review
commit ae1f8f888a
2 changed files with 153 additions and 10 deletions

View File

@ -18,8 +18,10 @@
import argparse
import getpass
import io
import json
import logging
import os
import urllib.parse
from cliff import columns as cliff_columns
import iso8601
@ -681,7 +683,7 @@ class NICAction(argparse.Action):
class BDMLegacyAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
# Make sure we have an empty dict rather than None
# Make sure we have an empty list rather than None
if getattr(namespace, self.dest, None) is None:
setattr(namespace, self.dest, [])
@ -723,6 +725,68 @@ class BDMLegacyAction(argparse.Action):
getattr(namespace, self.dest).append(mapping)
class BDMAction(parseractions.MultiKeyValueAction):
def __init__(self, option_strings, dest, **kwargs):
required_keys = []
optional_keys = [
'uuid', 'source_type', 'destination_type',
'disk_bus', 'device_type', 'device_name', 'volume_size',
'guest_format', 'boot_index', 'delete_on_termination', 'tag',
'volume_type',
]
super().__init__(
option_strings, dest, required_keys=required_keys,
optional_keys=optional_keys, **kwargs,
)
# TODO(stephenfin): Remove once I549d0897ef3704b7f47000f867d6731ad15d3f2b
# or similar lands in a release
def validate_keys(self, keys):
"""Validate the provided keys.
:param keys: A list of keys to validate.
"""
valid_keys = self.required_keys | self.optional_keys
invalid_keys = [k for k in keys if k not in valid_keys]
if invalid_keys:
msg = _(
"Invalid keys %(invalid_keys)s specified.\n"
"Valid keys are: %(valid_keys)s"
)
raise argparse.ArgumentTypeError(msg % {
'invalid_keys': ', '.join(invalid_keys),
'valid_keys': ', '.join(valid_keys),
})
missing_keys = [k for k in self.required_keys if k not in keys]
if missing_keys:
msg = _(
"Missing required keys %(missing_keys)s.\n"
"Required keys are: %(required_keys)s"
)
raise argparse.ArgumentTypeError(msg % {
'missing_keys': ', '.join(missing_keys),
'required_keys': ', '.join(self.required_keys),
})
def __call__(self, parser, namespace, values, option_string=None):
if getattr(namespace, self.dest, None) is None:
setattr(namespace, self.dest, [])
if values.startswith('file://'):
path = urllib.parse.urlparse(values).path
with open(path) as fh:
data = json.load(fh)
# Validate the keys - other validation is left to later
self.validate_keys(list(data))
getattr(namespace, self.dest, []).append(data)
else:
super().__call__(parser, namespace, values, option_string)
class CreateServer(command.ShowOne):
_description = _("Create a new server")
@ -829,19 +893,15 @@ class CreateServer(command.ShowOne):
parser.add_argument(
'--block-device',
metavar='',
action=parseractions.MultiKeyValueAction,
action=BDMAction,
dest='block_devices',
default=[],
required_keys=[],
optional_keys=[
'uuid', 'source_type', 'destination_type',
'disk_bus', 'device_type', 'device_name', 'volume_size',
'guest_format', 'boot_index', 'delete_on_termination', 'tag',
'volume_type',
],
help=_(
'Create a block device on the server.\n'
'Block device in the format:\n'
'Either a URI-style path (\'file:\\\\{path}\') to a JSON file '
'or a CSV-serialized string describing the block device '
'mapping.\n'
'The following keys are accepted:\n'
'uuid=<uuid>: UUID of the volume, snapshot or ID '
'(required if using source image, snapshot or volume),\n'
'source_type=<source_type>: source type '

View File

@ -16,6 +16,8 @@ import argparse
import collections
import copy
import getpass
import json
import tempfile
from unittest import mock
from unittest.mock import call
@ -2169,6 +2171,87 @@ class TestServerCreate(TestServer):
self.assertEqual(self.columns, columns)
self.assertEqual(self.datalist(), data)
def test_server_create_with_block_device_from_file(self):
self.app.client_manager.compute.api_version = api_versions.APIVersion(
'2.67')
block_device = {
'uuid': self.volume.id,
'source_type': 'volume',
'destination_type': 'volume',
'disk_bus': 'ide',
'device_type': 'disk',
'device_name': 'sdb',
'guest_format': 'ext4',
'volume_size': 64,
'volume_type': 'foo',
'boot_index': 1,
'delete_on_termination': True,
'tag': 'foo',
}
with tempfile.NamedTemporaryFile(mode='w+') as fp:
json.dump(block_device, fp=fp)
fp.flush()
arglist = [
'--image', 'image1',
'--flavor', self.flavor.id,
'--block-device', f'file://{fp.name}',
self.new_server.name,
]
verifylist = [
('image', 'image1'),
('flavor', self.flavor.id),
('block_devices', [block_device]),
('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 = {
'meta': None,
'files': {},
'reservation_id': None,
'min_count': 1,
'max_count': 1,
'security_groups': [],
'userdata': None,
'key_name': None,
'availability_zone': None,
'admin_pass': None,
'block_device_mapping_v2': [{
'uuid': self.volume.id,
'source_type': 'volume',
'destination_type': 'volume',
'disk_bus': 'ide',
'device_name': 'sdb',
'volume_size': 64,
'guest_format': 'ext4',
'boot_index': 1,
'device_type': 'disk',
'delete_on_termination': True,
'tag': 'foo',
'volume_type': 'foo',
}],
'nics': 'auto',
'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_invalid_boot_index(self):
block_device = \
f'uuid={self.volume.name},source_type=volume,boot_index=foo'