Add 'port create' command
This patch adds usage of 'port create' in CLI Change-Id: I888af50784c3b6c7ec30552ade79f05a5e974711 Partial-bug: #1519909 Partially-implements: blueprint neutron-client
This commit is contained in:
		| @@ -4,6 +4,84 @@ port | ||||
|  | ||||
| Network v2 | ||||
|  | ||||
| port create | ||||
| ----------- | ||||
|  | ||||
| Create new port | ||||
|  | ||||
| .. program:: port create | ||||
| .. code:: bash | ||||
|  | ||||
|     os port create | ||||
|         --network <network> | ||||
|         [--fixed-ip subnet=<subnet>,ip-address=<ip-address>] | ||||
|         [--device-id <device-id>] | ||||
|         [--device-owner <device-owner>] | ||||
|         [--vnic-type <vnic-type>] | ||||
|         [--binding-profile <binding-profile>] | ||||
|         [--host-id <host-id>] | ||||
|         [--enable | --disable] | ||||
|         [--mac-address <mac-address>] | ||||
|         [--project <project> [--project-domain <project-domain>]] | ||||
|         <name> | ||||
|  | ||||
| .. option:: --network <network> | ||||
|  | ||||
|     Network this port belongs to (name or ID) | ||||
|  | ||||
| .. option:: --fixed-ip subnet=<subnet>,ip-address=<ip-address> | ||||
|  | ||||
|     Desired IP and/or subnet (name or ID) for this port: | ||||
|     subnet=<subnet>,ip-address=<ip-address> | ||||
|     (this option can be repeated) | ||||
|  | ||||
| .. option:: --device-id <device-id> | ||||
|  | ||||
|     Device ID of this port | ||||
|  | ||||
| .. option:: --device-owner <device-owner> | ||||
|  | ||||
|     Device owner of this port | ||||
|  | ||||
| .. option:: --vnic-type <vnic-type> | ||||
|  | ||||
|     VNIC type for this port (direct | direct-physical | macvtap | normal(default) | baremetal) | ||||
|  | ||||
| .. option:: --binding-profile <binding-profile> | ||||
|  | ||||
|     Custom data to be passed as binding:profile: <key>=<value> | ||||
|     (this option can be repeated) | ||||
|  | ||||
| .. option:: --host-id <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> | ||||
|  | ||||
|     MAC address of this port | ||||
|  | ||||
| .. option:: --project <project> | ||||
|  | ||||
|     Owner's project (name or ID) | ||||
|  | ||||
| .. option:: --project-domain <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> | ||||
|  | ||||
|     Name of this port | ||||
|  | ||||
| port delete | ||||
| ----------- | ||||
|  | ||||
|   | ||||
| @@ -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=<subnet>,ip-address=<ip-address>', | ||||
|             action=parseractions.MultiKeyValueAction, | ||||
|             optional_keys=['subnet', 'ip-address'], | ||||
|             help='Desired IP and/or subnet (name or ID) for this port: ' | ||||
|                  'subnet=<subnet>,ip-address=<ip-address> ' | ||||
|                  '(this option can be repeated)') | ||||
|         parser.add_argument( | ||||
|             '--device-id', | ||||
|             metavar='<device-id>', | ||||
|             help='Device ID of this port') | ||||
|         parser.add_argument( | ||||
|             '--device-owner', | ||||
|             metavar='<device-owner>', | ||||
|             help='Device owner of this port') | ||||
|         parser.add_argument( | ||||
|             '--vnic-type', | ||||
|             metavar='<vnic-type>', | ||||
|             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='<binding-profile>', | ||||
|             action=parseractions.KeyValueAction, | ||||
|             help='Custom data to be passed as binding:profile: <key>=<value> ' | ||||
|                  '(this option can be repeated)') | ||||
|         parser.add_argument( | ||||
|             '--host-id', | ||||
|             metavar='<host-id>', | ||||
|             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='<network>', | ||||
|             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='<mac-address>', | ||||
|             help='MAC address of this port') | ||||
|         parser.add_argument( | ||||
|             '--project', | ||||
|             metavar='<project>', | ||||
|             help="Owner's project (name or ID)") | ||||
|         parser.add_argument( | ||||
|             'name', | ||||
|             metavar='<name>', | ||||
|             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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -0,0 +1,5 @@ | ||||
| --- | ||||
| features: | ||||
|   - | | ||||
|     Add support for the ``port create`` command. | ||||
|     [Bug `1519909 <https://bugs.launchpad.net/python-openstackclient/+bug/1519909>`_] | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jas
					Jas