Skip to content

Commit ae1f8f8

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "compute: Add support for loading BDMs from files"
2 parents e6b0473 + d3bd014 commit ae1f8f8

2 files changed

Lines changed: 153 additions & 10 deletions

File tree

openstackclient/compute/v2/server.py

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import argparse
1919
import getpass
2020
import io
21+
import json
2122
import logging
2223
import os
24+
import urllib.parse
2325

2426
from cliff import columns as cliff_columns
2527
import iso8601
@@ -681,7 +683,7 @@ def __call__(self, parser, namespace, values, option_string=None):
681683
class BDMLegacyAction(argparse.Action):
682684

683685
def __call__(self, parser, namespace, values, option_string=None):
684-
# Make sure we have an empty dict rather than None
686+
# Make sure we have an empty list rather than None
685687
if getattr(namespace, self.dest, None) is None:
686688
setattr(namespace, self.dest, [])
687689

@@ -723,6 +725,68 @@ def __call__(self, parser, namespace, values, option_string=None):
723725
getattr(namespace, self.dest).append(mapping)
724726

725727

728+
class BDMAction(parseractions.MultiKeyValueAction):
729+
730+
def __init__(self, option_strings, dest, **kwargs):
731+
required_keys = []
732+
optional_keys = [
733+
'uuid', 'source_type', 'destination_type',
734+
'disk_bus', 'device_type', 'device_name', 'volume_size',
735+
'guest_format', 'boot_index', 'delete_on_termination', 'tag',
736+
'volume_type',
737+
]
738+
super().__init__(
739+
option_strings, dest, required_keys=required_keys,
740+
optional_keys=optional_keys, **kwargs,
741+
)
742+
743+
# TODO(stephenfin): Remove once I549d0897ef3704b7f47000f867d6731ad15d3f2b
744+
# or similar lands in a release
745+
def validate_keys(self, keys):
746+
"""Validate the provided keys.
747+
748+
:param keys: A list of keys to validate.
749+
"""
750+
valid_keys = self.required_keys | self.optional_keys
751+
invalid_keys = [k for k in keys if k not in valid_keys]
752+
if invalid_keys:
753+
msg = _(
754+
"Invalid keys %(invalid_keys)s specified.\n"
755+
"Valid keys are: %(valid_keys)s"
756+
)
757+
raise argparse.ArgumentTypeError(msg % {
758+
'invalid_keys': ', '.join(invalid_keys),
759+
'valid_keys': ', '.join(valid_keys),
760+
})
761+
762+
missing_keys = [k for k in self.required_keys if k not in keys]
763+
if missing_keys:
764+
msg = _(
765+
"Missing required keys %(missing_keys)s.\n"
766+
"Required keys are: %(required_keys)s"
767+
)
768+
raise argparse.ArgumentTypeError(msg % {
769+
'missing_keys': ', '.join(missing_keys),
770+
'required_keys': ', '.join(self.required_keys),
771+
})
772+
773+
def __call__(self, parser, namespace, values, option_string=None):
774+
if getattr(namespace, self.dest, None) is None:
775+
setattr(namespace, self.dest, [])
776+
777+
if values.startswith('file://'):
778+
path = urllib.parse.urlparse(values).path
779+
with open(path) as fh:
780+
data = json.load(fh)
781+
782+
# Validate the keys - other validation is left to later
783+
self.validate_keys(list(data))
784+
785+
getattr(namespace, self.dest, []).append(data)
786+
else:
787+
super().__call__(parser, namespace, values, option_string)
788+
789+
726790
class CreateServer(command.ShowOne):
727791
_description = _("Create a new server")
728792

@@ -829,19 +893,15 @@ def get_parser(self, prog_name):
829893
parser.add_argument(
830894
'--block-device',
831895
metavar='',
832-
action=parseractions.MultiKeyValueAction,
896+
action=BDMAction,
833897
dest='block_devices',
834898
default=[],
835-
required_keys=[],
836-
optional_keys=[
837-
'uuid', 'source_type', 'destination_type',
838-
'disk_bus', 'device_type', 'device_name', 'volume_size',
839-
'guest_format', 'boot_index', 'delete_on_termination', 'tag',
840-
'volume_type',
841-
],
842899
help=_(
843900
'Create a block device on the server.\n'
844-
'Block device in the format:\n'
901+
'Either a URI-style path (\'file:\\\\{path}\') to a JSON file '
902+
'or a CSV-serialized string describing the block device '
903+
'mapping.\n'
904+
'The following keys are accepted:\n'
845905
'uuid=<uuid>: UUID of the volume, snapshot or ID '
846906
'(required if using source image, snapshot or volume),\n'
847907
'source_type=<source_type>: source type '

openstackclient/tests/unit/compute/v2/test_server.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import collections
1717
import copy
1818
import getpass
19+
import json
20+
import tempfile
1921
from unittest import mock
2022
from unittest.mock import call
2123

@@ -2169,6 +2171,87 @@ def test_server_create_with_block_device_full(self):
21692171
self.assertEqual(self.columns, columns)
21702172
self.assertEqual(self.datalist(), data)
21712173

2174+
def test_server_create_with_block_device_from_file(self):
2175+
self.app.client_manager.compute.api_version = api_versions.APIVersion(
2176+
'2.67')
2177+
2178+
block_device = {
2179+
'uuid': self.volume.id,
2180+
'source_type': 'volume',
2181+
'destination_type': 'volume',
2182+
'disk_bus': 'ide',
2183+
'device_type': 'disk',
2184+
'device_name': 'sdb',
2185+
'guest_format': 'ext4',
2186+
'volume_size': 64,
2187+
'volume_type': 'foo',
2188+
'boot_index': 1,
2189+
'delete_on_termination': True,
2190+
'tag': 'foo',
2191+
}
2192+
2193+
with tempfile.NamedTemporaryFile(mode='w+') as fp:
2194+
json.dump(block_device, fp=fp)
2195+
fp.flush()
2196+
2197+
arglist = [
2198+
'--image', 'image1',
2199+
'--flavor', self.flavor.id,
2200+
'--block-device', f'file://{fp.name}',
2201+
self.new_server.name,
2202+
]
2203+
verifylist = [
2204+
('image', 'image1'),
2205+
('flavor', self.flavor.id),
2206+
('block_devices', [block_device]),
2207+
('server_name', self.new_server.name),
2208+
]
2209+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
2210+
2211+
# CreateServer.take_action() returns two tuples
2212+
columns, data = self.cmd.take_action(parsed_args)
2213+
2214+
# Set expected values
2215+
kwargs = {
2216+
'meta': None,
2217+
'files': {},
2218+
'reservation_id': None,
2219+
'min_count': 1,
2220+
'max_count': 1,
2221+
'security_groups': [],
2222+
'userdata': None,
2223+
'key_name': None,
2224+
'availability_zone': None,
2225+
'admin_pass': None,
2226+
'block_device_mapping_v2': [{
2227+
'uuid': self.volume.id,
2228+
'source_type': 'volume',
2229+
'destination_type': 'volume',
2230+
'disk_bus': 'ide',
2231+
'device_name': 'sdb',
2232+
'volume_size': 64,
2233+
'guest_format': 'ext4',
2234+
'boot_index': 1,
2235+
'device_type': 'disk',
2236+
'delete_on_termination': True,
2237+
'tag': 'foo',
2238+
'volume_type': 'foo',
2239+
}],
2240+
'nics': 'auto',
2241+
'scheduler_hints': {},
2242+
'config_drive': None,
2243+
}
2244+
# ServerManager.create(name, image, flavor, **kwargs)
2245+
self.servers_mock.create.assert_called_with(
2246+
self.new_server.name,
2247+
self.image,
2248+
self.flavor,
2249+
**kwargs
2250+
)
2251+
2252+
self.assertEqual(self.columns, columns)
2253+
self.assertEqual(self.datalist(), data)
2254+
21722255
def test_server_create_with_block_device_invalid_boot_index(self):
21732256
block_device = \
21742257
f'uuid={self.volume.name},source_type=volume,boot_index=foo'

0 commit comments

Comments
 (0)