diff --git a/doc/source/command-objects/port.rst b/doc/source/command-objects/port.rst index 414b7437f..0c91f3ac0 100644 --- a/doc/source/command-objects/port.rst +++ b/doc/source/command-objects/port.rst @@ -4,6 +4,84 @@ port Network v2 +port create +----------- + +Create new port + +.. program:: port create +.. code:: bash + + os port create + --network + [--fixed-ip subnet=,ip-address=] + [--device-id ] + [--device-owner ] + [--vnic-type ] + [--binding-profile ] + [--host-id ] + [--enable | --disable] + [--mac-address ] + [--project [--project-domain ]] + + +.. option:: --network + + Network this port belongs to (name or ID) + +.. option:: --fixed-ip subnet=,ip-address= + + Desired IP and/or subnet (name or ID) for this port: + subnet=,ip-address= + (this option can be repeated) + +.. option:: --device-id + + Device ID of this port + +.. option:: --device-owner + + Device owner of this port + +.. option:: --vnic-type + + VNIC type for this port (direct | direct-physical | macvtap | normal(default) | baremetal) + +.. option:: --binding-profile + + Custom data to be passed as binding:profile: = + (this option can be repeated) + +.. option:: --host-id + + The ID of the host where the port is allocated + +.. option:: --enable + + Enable port (default) + +.. option:: --disable + + Disable port + +.. option:: --mac-address + + MAC address of this port + +.. option:: --project + + Owner's project (name or ID) + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. _port_create-name: +.. describe:: + + Name of this port + port delete ----------- diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 19b3701d8..f9d0fc957 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -14,13 +14,14 @@ """Port action implementations""" from openstackclient.common import command +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common as identity_common def _format_admin_state(state): return 'UP' if state else 'DOWN' - _formatters = { 'admin_state_up': _format_admin_state, 'allowed_address_pairs': utils.format_list_of_dicts, @@ -49,7 +50,171 @@ def _get_columns(item): if binding_column in columns: columns.remove(binding_column) columns.append(binding_column.replace('binding:', 'binding_', 1)) - return sorted(columns) + return tuple(sorted(columns)) + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if parsed_args.fixed_ip is not None: + attrs['fixed_ips'] = parsed_args.fixed_ip + if parsed_args.device_id is not None: + attrs['device_id'] = parsed_args.device_id + if parsed_args.device_owner is not None: + attrs['device_owner'] = parsed_args.device_owner + if parsed_args.admin_state is not None: + attrs['admin_state_up'] = parsed_args.admin_state + if parsed_args.binding_profile is not None: + attrs['binding:profile'] = parsed_args.binding_profile + if parsed_args.vnic_type is not None: + attrs['binding:vnic_type'] = parsed_args.vnic_type + if parsed_args.host_id is not None: + attrs['binding:host_id'] = parsed_args.host_id + + # The remaining options do not support 'port set' command, so they require + # additional check + if 'mac_address' in parsed_args and parsed_args.mac_address is not None: + attrs['mac_address'] = parsed_args.mac_address + if 'network' in parsed_args and parsed_args.network is not None: + attrs['network_id'] = parsed_args.network + if 'project' in parsed_args and parsed_args.project is not None: + # TODO(singhj): since 'project' logic is common among + # router, network, port etc., maybe move it to a common file. + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + return attrs + + +def _prepare_fixed_ips(client_manager, parsed_args): + """Fix and properly format fixed_ip option. + + Appropriately convert any subnet names to their respective ids. + Convert fixed_ips in parsed args to be in valid dictionary format: + {'subnet': 'foo'}. + """ + client = client_manager.network + ips = [] + + if parsed_args.fixed_ip: + for ip_spec in parsed_args.fixed_ip: + if 'subnet' in ip_spec: + subnet_name_id = ip_spec['subnet'] + if subnet_name_id: + _subnet = client.find_subnet(subnet_name_id, + ignore_missing=False) + ip_spec['subnet_id'] = _subnet.id + del ip_spec['subnet'] + + if 'ip-address' in ip_spec: + ip_spec['ip_address'] = ip_spec['ip-address'] + del ip_spec['ip-address'] + + ips.append(ip_spec) + + if ips: + parsed_args.fixed_ip = ips + + +def _add_updatable_args(parser): + parser.add_argument( + '--fixed-ip', + metavar='subnet=,ip-address=', + action=parseractions.MultiKeyValueAction, + optional_keys=['subnet', 'ip-address'], + help='Desired IP and/or subnet (name or ID) for this port: ' + 'subnet=,ip-address= ' + '(this option can be repeated)') + parser.add_argument( + '--device-id', + metavar='', + help='Device ID of this port') + parser.add_argument( + '--device-owner', + metavar='', + help='Device owner of this port') + parser.add_argument( + '--vnic-type', + metavar='', + choices=['direct', 'direct-physical', 'macvtap', + 'normal', 'baremetal'], + help='VNIC type for this port (direct | direct-physical |' + ' macvtap | normal(default) | baremetal)') + parser.add_argument( + '--binding-profile', + metavar='', + action=parseractions.KeyValueAction, + help='Custom data to be passed as binding:profile: = ' + '(this option can be repeated)') + parser.add_argument( + '--host-id', + metavar='', + help='The ID of the host where the port is allocated' + ) + + +class CreatePort(command.ShowOne): + """Create a new port""" + + def get_parser(self, prog_name): + parser = super(CreatePort, self).get_parser(prog_name) + + parser.add_argument( + '--network', + metavar='', + required=True, + help='Network this port belongs to (name or ID)') + _add_updatable_args(parser) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + dest='admin_state', + action='store_true', + default=True, + help='Enable port (default)', + ) + admin_group.add_argument( + '--disable', + dest='admin_state', + action='store_false', + help='Disable port', + ) + parser.add_argument( + '--mac-address', + metavar='', + help='MAC address of this port') + parser.add_argument( + '--project', + metavar='', + help="Owner's project (name or ID)") + parser.add_argument( + 'name', + metavar='', + help='Name of this port') + identity_common.add_project_domain_option_to_parser(parser) + # TODO(singhj): Add support for extended options: + # qos,security groups,dhcp, address pairs + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + _network = client.find_network(parsed_args.network, + ignore_missing=False) + parsed_args.network = _network.id + _prepare_fixed_ips(self.app.client_manager, parsed_args) + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_port(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + + return columns, data class DeletePort(command.Command): @@ -90,4 +255,4 @@ class ShowPort(command.ShowOne): obj = client.find_port(parsed_args.port, ignore_missing=False) columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) - return (tuple(columns), data) + return columns, data diff --git a/openstackclient/tests/network/v2/test_port.py b/openstackclient/tests/network/v2/test_port.py index bc246bd87..907d8a7d1 100644 --- a/openstackclient/tests/network/v2/test_port.py +++ b/openstackclient/tests/network/v2/test_port.py @@ -27,6 +27,150 @@ class TestPort(network_fakes.TestNetworkV2): # Get a shortcut to the network client self.network = self.app.client_manager.network + def _get_common_cols_data(self, fake_port): + columns = ( + 'admin_state_up', + 'allowed_address_pairs', + 'binding_host_id', + 'binding_profile', + 'binding_vif_details', + 'binding_vif_type', + 'binding_vnic_type', + 'device_id', + 'device_owner', + 'dns_assignment', + 'dns_name', + 'extra_dhcp_opts', + 'fixed_ips', + 'id', + 'mac_address', + 'name', + 'network_id', + 'port_security_enabled', + 'project_id', + 'security_groups', + 'status', + ) + + data = ( + port._format_admin_state(fake_port.admin_state_up), + utils.format_list_of_dicts(fake_port.allowed_address_pairs), + fake_port.binding_host_id, + utils.format_dict(fake_port.binding_profile), + utils.format_dict(fake_port.binding_vif_details), + fake_port.binding_vif_type, + fake_port.binding_vnic_type, + fake_port.device_id, + fake_port.device_owner, + utils.format_list_of_dicts(fake_port.dns_assignment), + fake_port.dns_name, + utils.format_list_of_dicts(fake_port.extra_dhcp_opts), + utils.format_list_of_dicts(fake_port.fixed_ips), + fake_port.id, + fake_port.mac_address, + fake_port.name, + fake_port.network_id, + fake_port.port_security_enabled, + fake_port.project_id, + utils.format_list(fake_port.security_groups), + fake_port.status, + ) + + return columns, data + + +class TestCreatePort(TestPort): + + _port = network_fakes.FakePort.create_one_port() + + def setUp(self): + super(TestCreatePort, self).setUp() + + self.network.create_port = mock.Mock(return_value=self._port) + fake_net = network_fakes.FakeNetwork.create_one_network({ + 'id': self._port.network_id, + }) + self.network.find_network = mock.Mock(return_value=fake_net) + self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() + self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) + # Get the command object to test + self.cmd = port.CreatePort(self.app, self.namespace) + + def test_create_default_options(self): + arglist = [ + '--network', self._port.network_id, + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id,), + ('admin_state', True), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_with(**{ + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + + def test_create_full_options(self): + arglist = [ + '--mac-address', 'aa:aa:aa:aa:aa:aa', + '--fixed-ip', 'subnet=%s,ip-address=10.0.0.2' + % self.fake_subnet.id, + '--device-id', 'deviceid', + '--device-owner', 'fakeowner', + '--disable', + '--vnic-type', 'macvtap', + '--binding-profile', 'foo=bar', + '--binding-profile', 'foo2=bar2', + '--network', self._port.network_id, + 'test-port', + + ] + verifylist = [ + ('mac_address', 'aa:aa:aa:aa:aa:aa'), + ( + 'fixed_ip', + [{'subnet': self.fake_subnet.id, 'ip-address': '10.0.0.2'}] + ), + ('device_id', 'deviceid'), + ('device_owner', 'fakeowner'), + ('admin_state', False), + ('vnic_type', 'macvtap'), + ('binding_profile', {'foo': 'bar', 'foo2': 'bar2'}), + ('network', self._port.network_id), + ('name', 'test-port'), + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = (self.cmd.take_action(parsed_args)) + + self.network.create_port.assert_called_with(**{ + 'mac_address': 'aa:aa:aa:aa:aa:aa', + 'fixed_ips': [{'subnet_id': self.fake_subnet.id, + 'ip_address': '10.0.0.2'}], + 'device_id': 'deviceid', + 'device_owner': 'fakeowner', + 'admin_state_up': False, + 'binding:vnic_type': 'macvtap', + 'binding:profile': {'foo': 'bar', 'foo2': 'bar2'}, + 'network_id': self._port.network_id, + 'name': 'test-port', + }) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) + class TestDeletePort(TestPort): @@ -60,54 +204,6 @@ class TestShowPort(TestPort): # The port to show. _port = network_fakes.FakePort.create_one_port() - columns = ( - 'admin_state_up', - 'allowed_address_pairs', - 'binding_host_id', - 'binding_profile', - 'binding_vif_details', - 'binding_vif_type', - 'binding_vnic_type', - 'device_id', - 'device_owner', - 'dns_assignment', - 'dns_name', - 'extra_dhcp_opts', - 'fixed_ips', - 'id', - 'mac_address', - 'name', - 'network_id', - 'port_security_enabled', - 'project_id', - 'security_groups', - 'status', - ) - - data = ( - port._format_admin_state(_port.admin_state_up), - utils.format_list_of_dicts(_port.allowed_address_pairs), - _port.binding_host_id, - utils.format_dict(_port.binding_profile), - utils.format_dict(_port.binding_vif_details), - _port.binding_vif_type, - _port.binding_vnic_type, - _port.device_id, - _port.device_owner, - utils.format_list_of_dicts(_port.dns_assignment), - _port.dns_name, - utils.format_list_of_dicts(_port.extra_dhcp_opts), - utils.format_list_of_dicts(_port.fixed_ips), - _port.id, - _port.mac_address, - _port.name, - _port.network_id, - _port.port_security_enabled, - _port.project_id, - utils.format_list(_port.security_groups), - _port.status, - ) - def setUp(self): super(TestShowPort, self).setUp() @@ -136,5 +232,7 @@ class TestShowPort(TestPort): self.network.find_port.assert_called_with(self._port.name, ignore_missing=False) - self.assertEqual(tuple(self.columns), columns) - self.assertEqual(self.data, data) + + ref_columns, ref_data = self._get_common_cols_data(self._port) + self.assertEqual(ref_columns, columns) + self.assertEqual(ref_data, data) diff --git a/releasenotes/notes/add-port-create-command-a3580662721a6312.yaml b/releasenotes/notes/add-port-create-command-a3580662721a6312.yaml new file mode 100644 index 000000000..4fafb42c0 --- /dev/null +++ b/releasenotes/notes/add-port-create-command-a3580662721a6312.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for the ``port create`` command. + [Bug `1519909 `_] diff --git a/setup.cfg b/setup.cfg index 284e6dec1..d00c5c5f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -331,6 +331,7 @@ openstack.network.v2 = network_list = openstackclient.network.v2.network:ListNetwork network_set = openstackclient.network.v2.network:SetNetwork network_show = openstackclient.network.v2.network:ShowNetwork + port_create = openstackclient.network.v2.port:CreatePort port_delete = openstackclient.network.v2.port:DeletePort port_show = openstackclient.network.v2.port:ShowPort router_create = openstackclient.network.v2.router:CreateRouter