Merge "compute: Improve 'server create --nic' option parsing"

This commit is contained in:
Zuul 2021-03-04 13:21:31 +00:00 committed by Gerrit Code Review
commit af18b514ef
2 changed files with 185 additions and 140 deletions

View File

@ -98,16 +98,6 @@ def _get_ip_address(addresses, address_type, ip_address_family):
) )
def _prefix_checked_value(prefix):
def func(value):
if ',' in value or '=' in value:
msg = _("Invalid argument %s, "
"characters ',' and '=' are not allowed") % value
raise argparse.ArgumentTypeError(msg)
return prefix + value
return func
def _prep_server_detail(compute_client, image_client, server, refresh=True): def _prep_server_detail(compute_client, image_client, server, refresh=True):
"""Prepare the detailed server dict for printing """Prepare the detailed server dict for printing
@ -611,6 +601,81 @@ class AddServerVolume(command.Command):
) )
# TODO(stephenfin): Replace with 'MultiKeyValueAction' when we no longer
# support '--nic=auto' and '--nic=none'
class NICAction(argparse.Action):
def __init__(
self,
option_strings,
dest,
nargs=None,
const=None,
default=None,
type=None,
choices=None,
required=False,
help=None,
metavar=None,
key=None,
):
self.key = key
super().__init__(
option_strings=option_strings, dest=dest, nargs=nargs, const=const,
default=default, type=type, choices=choices, required=required,
help=help, metavar=metavar,
)
def __call__(self, parser, namespace, values, option_string=None):
# Make sure we have an empty dict rather than None
if getattr(namespace, self.dest, None) is None:
setattr(namespace, self.dest, [])
# Handle the special auto/none cases
if values in ('auto', 'none'):
getattr(namespace, self.dest).append(values)
return
if self.key:
if ',' in values or '=' in values:
msg = _(
"Invalid argument %s; characters ',' and '=' are not "
"allowed"
)
raise argparse.ArgumentTypeError(msg % values)
values = '='.join([self.key, values])
info = {
'net-id': '',
'port-id': '',
'v4-fixed-ip': '',
'v6-fixed-ip': '',
}
for kv_str in values.split(','):
k, sep, v = kv_str.partition("=")
if k not in info or not v:
msg = _(
"Invalid argument %s; argument must be of form "
"'net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,"
"port-id=port-uuid'"
)
raise argparse.ArgumentTypeError(msg % values)
info[k] = v
if info['net-id'] and info['port-id']:
msg = _(
'Invalid argument %s; either network or port should be '
'specified but not both'
)
raise argparse.ArgumentTypeError(msg % values)
getattr(namespace, self.dest).append(info)
class CreateServer(command.ShowOne): class CreateServer(command.ShowOne):
_description = _("Create a new server") _description = _("Create a new server")
@ -701,9 +766,10 @@ class CreateServer(command.ShowOne):
parser.add_argument( parser.add_argument(
'--network', '--network',
metavar="<network>", metavar="<network>",
action='append', dest='nics',
dest='nic', default=[],
type=_prefix_checked_value('net-id='), action=NICAction,
key='net-id',
# NOTE(RuiChen): Add '\n' to the end of line to improve formatting; # NOTE(RuiChen): Add '\n' to the end of line to improve formatting;
# see cliff's _SmartHelpFormatter for more details. # see cliff's _SmartHelpFormatter for more details.
help=_( help=_(
@ -719,9 +785,10 @@ class CreateServer(command.ShowOne):
parser.add_argument( parser.add_argument(
'--port', '--port',
metavar="<port>", metavar="<port>",
action='append', dest='nics',
dest='nic', default=[],
type=_prefix_checked_value('port-id='), action=NICAction,
key='port-id',
help=_( help=_(
"Create a NIC on the server and connect it to port. " "Create a NIC on the server and connect it to port. "
"Specify option multiple times to create multiple NICs. " "Specify option multiple times to create multiple NICs. "
@ -733,21 +800,28 @@ class CreateServer(command.ShowOne):
) )
parser.add_argument( parser.add_argument(
'--nic', '--nic',
metavar="<net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr," metavar="<net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr,"
"port-id=port-uuid,auto,none>", "v6-fixed-ip=ip-addr,auto,none>",
action='append', action=NICAction,
dest='nics',
default=[],
help=_( help=_(
"Create a NIC on the server. " "Create a NIC on the server.\n"
"Specify option multiple times to create multiple NICs. " "NIC in the format:\n"
"Either net-id or port-id must be provided, but not both. " "net-id=<net-uuid>: attach NIC to network with this UUID,\n"
"net-id: attach NIC to network with this UUID, " "port-id=<port-uuid>: attach NIC to port with this UUID,\n"
"port-id: attach NIC to port with this UUID, " "v4-fixed-ip=<ip-addr>: IPv4 fixed address for NIC (optional),"
"v4-fixed-ip: IPv4 fixed address for NIC (optional), " "\n"
"v6-fixed-ip: IPv6 fixed address for NIC (optional), " "v6-fixed-ip=<ip-addr>: IPv6 fixed address for NIC (optional),"
"none: (v2.37+) no network is attached, " "\n"
"none: (v2.37+) no network is attached,\n"
"auto: (v2.37+) the compute service will automatically " "auto: (v2.37+) the compute service will automatically "
"allocate a network. Specifying a --nic of auto or none " "allocate a network.\n"
"cannot be used with any other --nic value." "\n"
"Specify option multiple times to create multiple NICs.\n"
"Specifying a --nic of auto or none cannot be used with any "
"other --nic value.\n"
"Either net-id or port-id must be provided, but not both."
), ),
) )
parser.add_argument( parser.add_argument(
@ -1103,84 +1177,55 @@ class CreateServer(command.ShowOne):
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
block_device_mapping_v2.append(mapping) block_device_mapping_v2.append(mapping)
nics = [] nics = parsed_args.nics
auto_or_none = False
if parsed_args.nic is None:
parsed_args.nic = []
for nic_str in parsed_args.nic:
# Handle the special auto/none cases
if nic_str in ('auto', 'none'):
auto_or_none = True
nics.append(nic_str)
else:
nic_info = {
'net-id': '',
'v4-fixed-ip': '',
'v6-fixed-ip': '',
'port-id': '',
}
for kv_str in nic_str.split(","):
k, sep, v = kv_str.partition("=")
if k in nic_info and v:
nic_info[k] = v
else:
msg = _(
"Invalid nic argument '%s'. Nic arguments "
"must be of the form --nic <net-id=net-uuid"
",v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,"
"port-id=port-uuid>."
)
raise exceptions.CommandError(msg % k)
if bool(nic_info["net-id"]) == bool(nic_info["port-id"]): if 'auto' in nics or 'none' in nics:
msg = _( if len(nics) > 1:
'Either network or port should be specified ' msg = _(
'but not both' 'Specifying a --nic of auto or none cannot '
) 'be used with any other --nic, --network '
raise exceptions.CommandError(msg) 'or --port value.'
)
raise exceptions.CommandError(msg)
nics = nics[0]
else:
for nic in nics:
if self.app.client_manager.is_network_endpoint_enabled(): if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network network_client = self.app.client_manager.network
if nic_info["net-id"]:
if nic['net-id']:
net = network_client.find_network( net = network_client.find_network(
nic_info["net-id"], ignore_missing=False) nic['net-id'], ignore_missing=False,
nic_info["net-id"] = net.id )
if nic_info["port-id"]: nic['net-id'] = net.id
if nic['port-id']:
port = network_client.find_port( port = network_client.find_port(
nic_info["port-id"], ignore_missing=False) nic['port-id'], ignore_missing=False,
nic_info["port-id"] = port.id )
nic['port-id'] = port.id
else: else:
if nic_info["net-id"]: if nic['net-id']:
nic_info["net-id"] = compute_client.api.network_find( nic['net-id'] = compute_client.api.network_find(
nic_info["net-id"] nic['net-id'],
)['id'] )['id']
if nic_info["port-id"]:
if nic['port-id']:
msg = _( msg = _(
"Can't create server with port specified " "Can't create server with port specified "
"since network endpoint not enabled" "since network endpoint not enabled"
) )
raise exceptions.CommandError(msg) raise exceptions.CommandError(msg)
nics.append(nic_info) if not nics:
if nics:
if auto_or_none:
if len(nics) > 1:
msg = _(
'Specifying a --nic of auto or none cannot '
'be used with any other --nic, --network '
'or --port value.'
)
raise exceptions.CommandError(msg)
nics = nics[0]
else:
# Compute API version >= 2.37 requires a value, so default to # Compute API version >= 2.37 requires a value, so default to
# 'auto' to maintain legacy behavior if a nic wasn't specified. # 'auto' to maintain legacy behavior if a nic wasn't specified.
if compute_client.api_version >= api_versions.APIVersion('2.37'): if compute_client.api_version >= api_versions.APIVersion('2.37'):
nics = 'auto' nics = 'auto'
else: else:
# Default to empty list if nothing was specified, let nova # Default to empty list if nothing was specified and let nova
# side to decide the default behavior. # decide the default behavior.
nics = [] nics = []
# Check security group exist and convert ID to name # Check security group exist and convert ID to name

View File

@ -1311,8 +1311,28 @@ class TestServerCreate(TestServer):
verifylist = [ verifylist = [
('image', 'image1'), ('image', 'image1'),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['net-id=net1', 'net-id=net1,v4-fixed-ip=10.0.0.2', ('nics', [
'port-id=port1', 'net-id=net1', 'port-id=port2']), {
'net-id': 'net1', 'port-id': '',
'v4-fixed-ip': '', 'v6-fixed-ip': '',
},
{
'net-id': 'net1', 'port-id': '',
'v4-fixed-ip': '10.0.0.2', 'v6-fixed-ip': '',
},
{
'net-id': '', 'port-id': 'port1',
'v4-fixed-ip': '', 'v6-fixed-ip': '',
},
{
'net-id': 'net1', 'port-id': '',
'v4-fixed-ip': '', 'v6-fixed-ip': '',
},
{
'net-id': '', 'port-id': 'port2',
'v4-fixed-ip': '', 'v6-fixed-ip': '',
},
]),
('config_drive', False), ('config_drive', False),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
@ -1413,7 +1433,7 @@ class TestServerCreate(TestServer):
verifylist = [ verifylist = [
('image', 'image1'), ('image', 'image1'),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['auto']), ('nics', ['auto']),
('config_drive', False), ('config_drive', False),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
@ -1509,7 +1529,7 @@ class TestServerCreate(TestServer):
verifylist = [ verifylist = [
('image', 'image1'), ('image', 'image1'),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['none']), ('nics', ['none']),
('config_drive', False), ('config_drive', False),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
@ -1557,7 +1577,14 @@ class TestServerCreate(TestServer):
verifylist = [ verifylist = [
('image', 'image1'), ('image', 'image1'),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['none', 'auto', 'port-id=port1']), ('nics', [
'none',
'auto',
{
'net-id': '', 'port-id': 'port1',
'v4-fixed-ip': '', 'v6-fixed-ip': '',
},
]),
('config_drive', False), ('config_drive', False),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
@ -1587,17 +1614,10 @@ class TestServerCreate(TestServer):
'--nic', 'abcdefgh', '--nic', 'abcdefgh',
self.new_server.name, self.new_server.name,
] ]
verifylist = [ self.assertRaises(
('image', 'image1'), argparse.ArgumentTypeError,
('flavor', 'flavor1'), self.check_parser,
('nic', ['abcdefgh']), self.cmd, arglist, [])
('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)
self.assertNotCalled(self.servers_mock.create) self.assertNotCalled(self.servers_mock.create)
def test_server_create_with_invalid_network_key(self): def test_server_create_with_invalid_network_key(self):
@ -1607,17 +1627,10 @@ class TestServerCreate(TestServer):
'--nic', 'abcdefgh=12324', '--nic', 'abcdefgh=12324',
self.new_server.name, self.new_server.name,
] ]
verifylist = [ self.assertRaises(
('image', 'image1'), argparse.ArgumentTypeError,
('flavor', 'flavor1'), self.check_parser,
('nic', ['abcdefgh=12324']), self.cmd, arglist, [])
('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)
self.assertNotCalled(self.servers_mock.create) self.assertNotCalled(self.servers_mock.create)
def test_server_create_with_empty_network_key_value(self): def test_server_create_with_empty_network_key_value(self):
@ -1627,17 +1640,10 @@ class TestServerCreate(TestServer):
'--nic', 'net-id=', '--nic', 'net-id=',
self.new_server.name, self.new_server.name,
] ]
verifylist = [ self.assertRaises(
('image', 'image1'), argparse.ArgumentTypeError,
('flavor', 'flavor1'), self.check_parser,
('nic', ['net-id=']), self.cmd, arglist, [])
('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)
self.assertNotCalled(self.servers_mock.create) self.assertNotCalled(self.servers_mock.create)
def test_server_create_with_only_network_key(self): def test_server_create_with_only_network_key(self):
@ -1647,17 +1653,11 @@ class TestServerCreate(TestServer):
'--nic', 'net-id', '--nic', 'net-id',
self.new_server.name, self.new_server.name,
] ]
verifylist = [ self.assertRaises(
('image', 'image1'), argparse.ArgumentTypeError,
('flavor', 'flavor1'), self.check_parser,
('nic', ['net-id']), self.cmd, arglist, [])
('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)
self.assertNotCalled(self.servers_mock.create) self.assertNotCalled(self.servers_mock.create)
@mock.patch.object(common_utils, 'wait_for_status', return_value=True) @mock.patch.object(common_utils, 'wait_for_status', return_value=True)
@ -2230,7 +2230,7 @@ class TestServerCreate(TestServer):
verifylist = [ verifylist = [
('image_properties', {'hypervisor_type': 'qemu'}), ('image_properties', {'hypervisor_type': 'qemu'}),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['none']), ('nics', ['none']),
('config_drive', False), ('config_drive', False),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
@ -2286,7 +2286,7 @@ class TestServerCreate(TestServer):
('image_properties', {'hypervisor_type': 'qemu', ('image_properties', {'hypervisor_type': 'qemu',
'hw_disk_bus': 'ide'}), 'hw_disk_bus': 'ide'}),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['none']), ('nics', ['none']),
('config_drive', False), ('config_drive', False),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
@ -2342,7 +2342,7 @@ class TestServerCreate(TestServer):
('image_properties', {'hypervisor_type': 'qemu', ('image_properties', {'hypervisor_type': 'qemu',
'hw_disk_bus': 'virtio'}), 'hw_disk_bus': 'virtio'}),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['none']), ('nics', ['none']),
('config_drive', False), ('config_drive', False),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
@ -2374,7 +2374,7 @@ class TestServerCreate(TestServer):
('image_properties', ('image_properties',
{'owner_specified.openstack.object': 'image/cirros'}), {'owner_specified.openstack.object': 'image/cirros'}),
('flavor', 'flavor1'), ('flavor', 'flavor1'),
('nic', ['none']), ('nics', ['none']),
('server_name', self.new_server.name), ('server_name', self.new_server.name),
] ]
# create a image_info as the side_effect of the fake image_list() # create a image_info as the side_effect of the fake image_list()