diff options
Diffstat (limited to 'openstackclient/network')
| -rw-r--r-- | openstackclient/network/utils.py | 41 | ||||
| -rw-r--r-- | openstackclient/network/v2/floating_ip.py | 85 | ||||
| -rw-r--r-- | openstackclient/network/v2/network.py | 91 | ||||
| -rw-r--r-- | openstackclient/network/v2/port.py | 198 | ||||
| -rw-r--r-- | openstackclient/network/v2/router.py | 29 | ||||
| -rw-r--r-- | openstackclient/network/v2/security_group.py | 179 | ||||
| -rw-r--r-- | openstackclient/network/v2/security_group_rule.py | 141 | ||||
| -rw-r--r-- | openstackclient/network/v2/subnet.py | 289 | ||||
| -rw-r--r-- | openstackclient/network/v2/subnet_pool.py | 136 |
9 files changed, 1081 insertions, 108 deletions
diff --git a/openstackclient/network/utils.py b/openstackclient/network/utils.py new file mode 100644 index 00000000..287f0271 --- /dev/null +++ b/openstackclient/network/utils.py @@ -0,0 +1,41 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +# Transform compute security group rule for display. +def transform_compute_security_group_rule(sg_rule): + info = {} + info.update(sg_rule) + from_port = info.pop('from_port') + to_port = info.pop('to_port') + if isinstance(from_port, int) and isinstance(to_port, int): + port_range = {'port_range': "%u:%u" % (from_port, to_port)} + elif from_port is None and to_port is None: + port_range = {'port_range': ""} + else: + port_range = {'port_range': "%s:%s" % (from_port, to_port)} + info.update(port_range) + if 'cidr' in info['ip_range']: + info['ip_range'] = info['ip_range']['cidr'] + else: + info['ip_range'] = '' + if info['ip_protocol'] is None: + info['ip_protocol'] = '' + elif info['ip_protocol'].lower() == 'icmp': + info['port_range'] = '' + group = info.pop('group') + if 'name' in group: + info['remote_security_group'] = group['name'] + else: + info['remote_security_group'] = '' + return info diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index e0a65a48..b21d6e96 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -18,13 +18,96 @@ from openstackclient.network import common def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') return tuple(sorted(columns)) +def _get_attrs(client_manager, parsed_args): + attrs = {} + network_client = client_manager.network + + if parsed_args.network is not None: + network = network_client.find_network(parsed_args.network, + ignore_missing=False) + attrs['floating_network_id'] = network.id + + if parsed_args.subnet is not None: + subnet = network_client.find_subnet(parsed_args.subnet, + ignore_missing=False) + attrs['subnet_id'] = subnet.id + + if parsed_args.port is not None: + port = network_client.find_port(parsed_args.port, + ignore_missing=False) + attrs['port_id'] = port.id + + if parsed_args.floating_ip_address is not None: + attrs['floating_ip_address'] = parsed_args.floating_ip_address + + if parsed_args.fixed_ip_address is not None: + attrs['fixed_ip_address'] = parsed_args.fixed_ip_address + + return attrs + + +class CreateFloatingIP(common.NetworkAndComputeShowOne): + """Create floating IP""" + + def update_parser_common(self, parser): + # In Compute v2 network, floating IPs could be allocated from floating + # IP pools, which are actually external networks. So deprecate the + # parameter "pool", and use "network" instead. + parser.add_argument( + 'network', + metavar='<network>', + help='Network to allocate floating IP from (name or ID)', + ) + return parser + + def update_parser_network(self, parser): + parser.add_argument( + '--subnet', + metavar='<subnet>', + help="Subnet on which you want to create the floating IP " + "(name or ID)" + ) + parser.add_argument( + '--port', + metavar='<port>', + help="Port to be associated with the floating IP " + "(name or ID)" + ) + parser.add_argument( + '--floating-ip-address', + metavar='<floating-ip-address>', + dest='floating_ip_address', + help="Floating IP address" + ) + parser.add_argument( + '--fixed-ip-address', + metavar='<fixed-ip-address>', + dest='fixed_ip_address', + help="Fixed IP address mapped to the floating IP" + ) + return parser + + def take_action_network(self, client, parsed_args): + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_ip(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (columns, data) + + def take_action_compute(self, client, parsed_args): + obj = client.floating_ips.create(parsed_args.network) + columns = _get_columns(obj._info) + data = utils.get_dict_properties(obj._info, columns) + return (columns, data) + + class DeleteFloatingIP(common.NetworkAndComputeCommand): """Delete floating IP""" diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 612a1775..ebd5cb63 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -38,7 +38,7 @@ _formatters = { def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') @@ -52,10 +52,14 @@ def _get_attrs(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: attrs['name'] = str(parsed_args.name) - if parsed_args.admin_state is not None: - attrs['admin_state_up'] = parsed_args.admin_state - if parsed_args.shared is not None: - attrs['shared'] = parsed_args.shared + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + if parsed_args.share: + attrs['shared'] = True + if parsed_args.no_share: + attrs['shared'] = False # "network set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: @@ -79,8 +83,10 @@ def _get_attrs_compute(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: attrs['label'] = str(parsed_args.name) - if parsed_args.shared is not None: - attrs['share_address'] = parsed_args.shared + if parsed_args.share: + attrs['share_address'] = True + if parsed_args.no_share: + attrs['share_address'] = False if parsed_args.subnet is not None: attrs['cidr'] = parsed_args.subnet @@ -99,15 +105,13 @@ class CreateNetwork(common.NetworkAndComputeShowOne): share_group = parser.add_mutually_exclusive_group() share_group.add_argument( '--share', - dest='shared', action='store_true', default=None, help='Share the network between projects', ) share_group.add_argument( '--no-share', - dest='shared', - action='store_false', + action='store_true', help='Do not share the network between projects', ) return parser @@ -116,15 +120,13 @@ class CreateNetwork(common.NetworkAndComputeShowOne): admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state', action='store_true', default=True, help='Enable network (default)', ) admin_group.add_argument( '--disable', - dest='admin_state', - action='store_false', + action='store_true', help='Disable network', ) parser.add_argument( @@ -142,6 +144,47 @@ class CreateNetwork(common.NetworkAndComputeShowOne): '(requires the Network Availability Zone extension, ' 'this option can be repeated).', ) + external_router_grp = parser.add_mutually_exclusive_group() + external_router_grp.add_argument( + '--external', + action='store_true', + help='Set this network as an external network. ' + 'Requires the "external-net" extension to be enabled.') + external_router_grp.add_argument( + '--internal', + action='store_true', + help='Set this network as an internal network (default)') + default_router_grp = parser.add_mutually_exclusive_group() + default_router_grp.add_argument( + '--default', + action='store_true', + help='Specify if this network should be used as ' + 'the default external network') + default_router_grp.add_argument( + '--no-default', + action='store_true', + help='Do not use the network as the default external network.' + 'By default, no network is set as an external network.') + parser.add_argument( + '--provider-network-type', + metavar='<provider-network-type>', + choices=['flat', 'gre', 'local', + 'vlan', 'vxlan'], + help='The physical mechanism by which the virtual network ' + 'is implemented. The supported options are: ' + 'flat, gre, local, vlan, vxlan') + parser.add_argument( + '--provider-physical-network', + metavar='<provider-physical-network>', + dest='physical_network', + help='Name of the physical network over which the virtual ' + 'network is implemented') + parser.add_argument( + '--provider-segment', + metavar='<provider-segment>', + dest='segmentation_id', + help='VLAN ID for VLAN networks or Tunnel ID for GRE/VXLAN ' + 'networks') return parser def update_parser_compute(self, parser): @@ -154,6 +197,20 @@ class CreateNetwork(common.NetworkAndComputeShowOne): def take_action_network(self, client, parsed_args): attrs = _get_attrs(self.app.client_manager, parsed_args) + if parsed_args.internal: + attrs['router:external'] = False + if parsed_args.external: + attrs['router:external'] = True + if parsed_args.no_default: + attrs['is_default'] = False + if parsed_args.default: + attrs['is_default'] = True + if parsed_args.provider_network_type: + attrs['provider:network_type'] = parsed_args.provider_network_type + if parsed_args.physical_network: + attrs['provider:physical_network'] = parsed_args.physical_network + if parsed_args.segmentation_id: + attrs['provider:segmentation_id'] = parsed_args.segmentation_id obj = client.create_network(**attrs) columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) @@ -301,29 +358,25 @@ class SetNetwork(command.Command): admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state', action='store_true', default=None, help='Enable network', ) admin_group.add_argument( '--disable', - dest='admin_state', - action='store_false', + action='store_true', help='Disable network', ) share_group = parser.add_mutually_exclusive_group() share_group.add_argument( '--share', - dest='shared', action='store_true', default=None, help='Share the network between projects', ) share_group.add_argument( '--no-share', - dest='shared', - action='store_false', + action='store_true', help='Do not share the network between projects', ) return parser diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 449dcfd4..a9e80428 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -13,12 +13,20 @@ """Port action implementations""" +import argparse +import logging + from openstackclient.common import command +from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common as identity_common +LOG = logging.getLogger(__name__) + + def _format_admin_state(state): return 'UP' if state else 'DOWN' @@ -35,7 +43,7 @@ _formatters = { def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') @@ -56,23 +64,42 @@ def _get_columns(item): def _get_attrs(client_manager, parsed_args): attrs = {} - if parsed_args.name is not None: - attrs['name'] = str(parsed_args.name) + # Handle deprecated options + # NOTE(dtroyer): --device-id and --host-id were deprecated in Mar 2016. + # Do not remove before 3.x release or Mar 2017. + if parsed_args.device_id: + attrs['device_id'] = parsed_args.device_id + LOG.warning(_( + 'The --device-id option is deprecated, ' + 'please use --device instead.' + )) + if parsed_args.host_id: + attrs['binding:host_id'] = parsed_args.host_id + LOG.warning(_( + 'The --host-id option is deprecated, ' + 'please use --host instead.' + )) + 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: + attrs['device_id'] = parsed_args.device 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.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False 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 + if parsed_args.host: + attrs['binding:host_id'] = parsed_args.host + # It is possible that name is not updated during 'port set' + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) # 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: @@ -124,18 +151,19 @@ def _prepare_fixed_ips(client_manager, parsed_args): 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( + # NOTE(dtroyer): --device-id is deprecated in Mar 2016. Do not + # remove before 3.x release or Mar 2017. + device_group = parser.add_mutually_exclusive_group() + device_group.add_argument( + '--device', + metavar='<device-id>', + help='Port device ID', + ) + device_group.add_argument( '--device-id', metavar='<device-id>', - help='Device ID of this port') + help=argparse.SUPPRESS, + ) parser.add_argument( '--device-owner', metavar='<device-owner>', @@ -145,18 +173,21 @@ def _add_updatable_args(parser): 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( + help="VNIC type for this port (direct | direct-physical |" + " macvtap | normal | baremetal). If unspecified during" + " port creation, default value will be 'normal'.") + # NOTE(dtroyer): --host-id is deprecated in Mar 2016. Do not + # remove before 3.x release or Mar 2017. + host_group = parser.add_mutually_exclusive_group() + host_group.add_argument( + '--host', + metavar='<host-id>', + help='Allocate port on host <host-id> (ID only)', + ) + host_group.add_argument( '--host-id', metavar='<host-id>', - help='The ID of the host where the port is allocated' + help=argparse.SUPPRESS, ) @@ -172,18 +203,30 @@ class CreatePort(command.ShowOne): required=True, help='Network this port belongs to (name or ID)') _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( + '--binding-profile', + metavar='<binding-profile>', + action=parseractions.KeyValueAction, + help='Custom data to be passed as binding:profile: <key>=<value> ' + '(this option can be repeated)') 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', + action='store_true', help='Disable port', ) parser.add_argument( @@ -241,6 +284,16 @@ class DeletePort(command.Command): class ListPort(command.Lister): """List ports""" + def get_parser(self, prog_name): + parser = super(ListPort, self).get_parser(prog_name) + parser.add_argument( + '--router', + metavar='<router>', + dest='router', + help='List only ports attached to this router (name or ID)', + ) + return parser + def take_action(self, parsed_args): client = self.app.client_manager.network @@ -257,7 +310,14 @@ class ListPort(command.Lister): 'Fixed IP Addresses', ) - data = client.ports() + filters = {} + if parsed_args.router: + _router = client.find_router(parsed_args.router, + ignore_missing=False) + filters = {'device_id': _router.id} + + data = client.ports(**filters) + return (column_headers, (utils.get_item_properties( s, columns, @@ -265,6 +325,78 @@ class ListPort(command.Lister): ) for s in data)) +class SetPort(command.Command): + """Set port properties""" + + def get_parser(self, prog_name): + parser = super(SetPort, self).get_parser(prog_name) + _add_updatable_args(parser) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=None, + help='Enable port', + ) + admin_group.add_argument( + '--disable', + action='store_true', + help='Disable port', + ) + parser.add_argument( + '--name', + metavar="<name>", + help=('Set port name')) + parser.add_argument( + 'port', + metavar="<port>", + help=("Port to modify (name or ID)") + ) + fixed_ip = parser.add_mutually_exclusive_group() + fixed_ip.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)') + fixed_ip.add_argument( + '--no-fixed-ip', + action='store_true', + help='Clear existing information of fixed-ips') + binding_profile = parser.add_mutually_exclusive_group() + binding_profile.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)') + binding_profile.add_argument( + '--no-binding-profile', + action='store_true', + help='Clear existing information of binding:profile') + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + + _prepare_fixed_ips(self.app.client_manager, parsed_args) + attrs = _get_attrs(self.app.client_manager, parsed_args) + + if parsed_args.no_fixed_ip: + attrs['fixed_ips'] = [] + if parsed_args.no_binding_profile: + attrs['binding:profile'] = {} + + if attrs == {}: + msg = "Nothing specified to be set" + raise exceptions.CommandError(msg) + + obj = client.find_port(parsed_args.port, ignore_missing=False) + client.update_port(obj, **attrs) + + class ShowPort(command.ShowOne): """Display port details""" diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index caf6d5ce..cd0f0e4c 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -42,7 +42,7 @@ _formatters = { def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') @@ -53,10 +53,15 @@ def _get_attrs(client_manager, parsed_args): attrs = {} if parsed_args.name is not None: attrs['name'] = str(parsed_args.name) - if parsed_args.admin_state_up is not None: - attrs['admin_state_up'] = parsed_args.admin_state_up - if parsed_args.distributed is not None: - attrs['distributed'] = parsed_args.distributed + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + # centralized is available only for SetRouter and not for CreateRouter + if 'centralized' in parsed_args and parsed_args.centralized: + attrs['distributed'] = False + if parsed_args.distributed: + attrs['distributed'] = True if ('availability_zone_hints' in parsed_args and parsed_args.availability_zone_hints is not None): attrs['availability_zone_hints'] = parsed_args.availability_zone_hints @@ -119,15 +124,13 @@ class CreateRouter(command.ShowOne): admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state_up', action='store_true', default=True, help="Enable router (default)", ) admin_group.add_argument( '--disable', - dest='admin_state_up', - action='store_false', + action='store_true', help="Disable router", ) parser.add_argument( @@ -283,29 +286,24 @@ class SetRouter(command.Command): admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', - dest='admin_state_up', action='store_true', default=None, help='Enable router', ) admin_group.add_argument( '--disable', - dest='admin_state_up', - action='store_false', + action='store_true', help='Disable router', ) distribute_group = parser.add_mutually_exclusive_group() distribute_group.add_argument( '--distributed', - dest='distributed', action='store_true', - default=None, help="Set router to distributed mode (disabled router only)", ) distribute_group.add_argument( '--centralized', - dest='distributed', - action='store_false', + action='store_true', help="Set router to centralized mode (disabled router only)", ) routes_group = parser.add_mutually_exclusive_group() @@ -323,7 +321,6 @@ class SetRouter(command.Command): ) routes_group.add_argument( '--clear-routes', - dest='clear_routes', action='store_true', help="Clear routes associated with the router", ) diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index 62699ffd..92498144 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -14,9 +14,152 @@ """Security Group action implementations""" import argparse +import six from openstackclient.common import utils +from openstackclient.identity import common as identity_common from openstackclient.network import common +from openstackclient.network import utils as network_utils + + +def _format_network_security_group_rules(sg_rules): + # For readability and to align with formatting compute security group + # rules, trim keys with caller known (e.g. security group and tenant ID) + # or empty values. + for sg_rule in sg_rules: + empty_keys = [k for k, v in six.iteritems(sg_rule) if not v] + for key in empty_keys: + sg_rule.pop(key) + sg_rule.pop('security_group_id', None) + sg_rule.pop('tenant_id', None) + return utils.format_list_of_dicts(sg_rules) + + +def _format_compute_security_group_rule(sg_rule): + info = network_utils.transform_compute_security_group_rule(sg_rule) + # Trim parent security group ID since caller has this information. + info.pop('parent_group_id', None) + # Trim keys with empty string values. + keys_to_trim = [ + 'ip_protocol', + 'ip_range', + 'port_range', + 'remote_security_group', + ] + for key in keys_to_trim: + if key in info and not info[key]: + info.pop(key) + return utils.format_dict(info) + + +def _format_compute_security_group_rules(sg_rules): + rules = [] + for sg_rule in sg_rules: + rules.append(_format_compute_security_group_rule(sg_rule)) + return utils.format_list(rules, separator='\n') + + +_formatters_network = { + 'security_group_rules': _format_network_security_group_rules, +} + + +_formatters_compute = { + 'rules': _format_compute_security_group_rules, +} + + +def _get_columns(item): + # Build the display columns and a list of the property columns + # that need to be mapped (display column name, property name). + columns = list(item.keys()) + property_column_mappings = [] + if 'security_group_rules' in columns: + columns.append('rules') + columns.remove('security_group_rules') + property_column_mappings.append(('rules', 'security_group_rules')) + if 'tenant_id' in columns: + columns.append('project_id') + columns.remove('tenant_id') + property_column_mappings.append(('project_id', 'tenant_id')) + display_columns = sorted(columns) + + # Build the property columns and apply any column mappings. + property_columns = sorted(columns) + for property_column_mapping in property_column_mappings: + property_index = property_columns.index(property_column_mapping[0]) + property_columns[property_index] = property_column_mapping[1] + return tuple(display_columns), property_columns + + +class CreateSecurityGroup(common.NetworkAndComputeShowOne): + """Create a new security group""" + + def update_parser_common(self, parser): + parser.add_argument( + "name", + metavar="<name>", + help="New security group name", + ) + parser.add_argument( + "--description", + metavar="<description>", + help="Security group description", + ) + return parser + + def update_parser_network(self, parser): + parser.add_argument( + '--project', + metavar='<project>', + help="Owner's project (name or ID)" + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def _get_description(self, parsed_args): + if parsed_args.description is not None: + return parsed_args.description + else: + return parsed_args.name + + def take_action_network(self, client, parsed_args): + # Build the create attributes. + attrs = {} + attrs['name'] = parsed_args.name + attrs['description'] = self._get_description(parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + # Create the security group and display the results. + obj = client.create_security_group(**attrs) + display_columns, property_columns = _get_columns(obj) + data = utils.get_item_properties( + obj, + property_columns, + formatters=_formatters_network + ) + return (display_columns, data) + + def take_action_compute(self, client, parsed_args): + description = self._get_description(parsed_args) + obj = client.security_groups.create( + parsed_args.name, + description, + ) + display_columns, property_columns = _get_columns(obj._info) + data = utils.get_dict_properties( + obj._info, + property_columns, + formatters=_formatters_compute + ) + return (display_columns, data) class DeleteSecurityGroup(common.NetworkAndComputeCommand): @@ -143,3 +286,39 @@ class SetSecurityGroup(common.NetworkAndComputeCommand): data.name, data.description, ) + + +class ShowSecurityGroup(common.NetworkAndComputeShowOne): + """Display security group details""" + + def update_parser_common(self, parser): + parser.add_argument( + 'group', + metavar='<group>', + help='Security group to display (name or ID)', + ) + return parser + + def take_action_network(self, client, parsed_args): + obj = client.find_security_group(parsed_args.group, + ignore_missing=False) + display_columns, property_columns = _get_columns(obj) + data = utils.get_item_properties( + obj, + property_columns, + formatters=_formatters_network + ) + return (display_columns, data) + + def take_action_compute(self, client, parsed_args): + obj = utils.find_resource( + client.security_groups, + parsed_args.group, + ) + display_columns, property_columns = _get_columns(obj._info) + data = utils.get_dict_properties( + obj._info, + property_columns, + formatters=_formatters_compute + ) + return (display_columns, data) diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index a61e3233..f60995ab 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -16,51 +16,132 @@ import six from openstackclient.common import exceptions +from openstackclient.common import parseractions from openstackclient.common import utils from openstackclient.network import common - - -def _xform_security_group_rule(sgroup): - info = {} - info.update(sgroup) - from_port = info.pop('from_port') - to_port = info.pop('to_port') - if isinstance(from_port, int) and isinstance(to_port, int): - port_range = {'port_range': "%u:%u" % (from_port, to_port)} - elif from_port is None and to_port is None: - port_range = {'port_range': ""} - else: - port_range = {'port_range': "%s:%s" % (from_port, to_port)} - info.update(port_range) - if 'cidr' in info['ip_range']: - info['ip_range'] = info['ip_range']['cidr'] - else: - info['ip_range'] = '' - if info['ip_protocol'] is None: - info['ip_protocol'] = '' - elif info['ip_protocol'].lower() == 'icmp': - info['port_range'] = '' - group = info.pop('group') - if 'name' in group: - info['remote_security_group'] = group['name'] - else: - info['remote_security_group'] = '' - return info +from openstackclient.network import utils as network_utils def _format_security_group_rule_show(obj): - data = _xform_security_group_rule(obj) + data = network_utils.transform_compute_security_group_rule(obj) return zip(*sorted(six.iteritems(data))) def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') return tuple(sorted(columns)) +def _convert_to_lowercase(string): + return string.lower() + + +class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): + """Create a new security group rule""" + + def update_parser_common(self, parser): + parser.add_argument( + 'group', + metavar='<group>', + help='Create rule in this security group (name or ID)', + ) + # TODO(rtheis): Add support for additional protocols for network. + # Until then, continue enforcing the compute choices. + parser.add_argument( + "--proto", + metavar="<proto>", + default="tcp", + choices=['icmp', 'tcp', 'udp'], + type=_convert_to_lowercase, + help="IP protocol (icmp, tcp, udp; default: tcp)", + ) + source_group = parser.add_mutually_exclusive_group() + source_group.add_argument( + "--src-ip", + metavar="<ip-address>", + default="0.0.0.0/0", + help="Source IP address block (may use CIDR notation; default: " + "0.0.0.0/0)", + ) + source_group.add_argument( + "--src-group", + metavar="<group>", + help="Source security group (name or ID)", + ) + parser.add_argument( + "--dst-port", + metavar="<port-range>", + default=(0, 0), + action=parseractions.RangeAction, + help="Destination port, may be a single port or port range: " + "137:139 (only required for IP protocols tcp and udp)", + ) + return parser + + def take_action_network(self, client, parsed_args): + # Get the security group ID to hold the rule. + security_group_id = client.find_security_group( + parsed_args.group, + ignore_missing=False + ).id + + # Build the create attributes. + attrs = {} + # TODO(rtheis): Add --direction option. Until then, continue + # with the default of 'ingress'. + attrs['direction'] = 'ingress' + # TODO(rtheis): Add --ethertype option. Until then, continue + # with the default of 'IPv4' + attrs['ethertype'] = 'IPv4' + # TODO(rtheis): Add port range support (type and code) for icmp + # protocol. Until then, continue ignoring the port range. + if parsed_args.proto != 'icmp': + attrs['port_range_min'] = parsed_args.dst_port[0] + attrs['port_range_max'] = parsed_args.dst_port[1] + attrs['protocol'] = parsed_args.proto + if parsed_args.src_group is not None: + attrs['remote_group_id'] = client.find_security_group( + parsed_args.src_group, + ignore_missing=False + ).id + else: + attrs['remote_ip_prefix'] = parsed_args.src_ip + attrs['security_group_id'] = security_group_id + + # Create and show the security group rule. + obj = client.create_security_group_rule(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (columns, data) + + def take_action_compute(self, client, parsed_args): + group = utils.find_resource( + client.security_groups, + parsed_args.group, + ) + if parsed_args.proto == 'icmp': + from_port, to_port = -1, -1 + else: + from_port, to_port = parsed_args.dst_port + if parsed_args.src_group is not None: + parsed_args.src_group = utils.find_resource( + client.security_groups, + parsed_args.src_group, + ).id + obj = client.security_group_rules.create( + group.id, + parsed_args.proto, + from_port, + to_port, + parsed_args.src_ip, + parsed_args.src_group, + ) + return _format_security_group_rule_show(obj._info) + + class DeleteSecurityGroupRule(common.NetworkAndComputeCommand): """Delete a security group rule""" diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index b514a88f..10e5859a 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -12,9 +12,15 @@ # """Subnet action implementations""" +import copy + +from json.encoder import JSONEncoder from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common as identity_common def _format_allocation_pools(data): @@ -23,21 +29,246 @@ def _format_allocation_pools(data): return ','.join(pool_formatted) +def _format_host_routes(data): + try: + return '\n'.join([JSONEncoder().encode(route) for route in data]) + except (TypeError, KeyError): + return '' + + _formatters = { 'allocation_pools': _format_allocation_pools, 'dns_nameservers': utils.format_list, - 'host_routes': utils.format_list, + 'host_routes': _format_host_routes, } +def _get_common_parse_arguments(parser): + parser.add_argument( + '--allocation-pool', + metavar='start=<ip-address>,end=<ip-address>', + dest='allocation_pools', + action=parseractions.MultiKeyValueAction, + required_keys=['start', 'end'], + help='Allocation pool IP addresses for this subnet ' + 'e.g.: start=192.168.199.2,end=192.168.199.254 ' + '(This option can be repeated)', + ) + parser.add_argument( + '--dns-nameserver', + metavar='<dns-nameserver>', + action='append', + dest='dns_nameservers', + help='DNS name server for this subnet ' + '(This option can be repeated)', + ) + parser.add_argument( + '--host-route', + metavar='destination=<subnet>,gateway=<ip-address>', + dest='host_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['destination', 'gateway'], + help='Additional route for this subnet ' + 'e.g.: destination=10.10.0.0/16,gateway=192.168.71.254 ' + 'destination: destination subnet (in CIDR notation) ' + 'gateway: nexthop IP address ' + '(This option can be repeated)', + ) + + def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') return tuple(sorted(columns)) +def convert_entries_to_nexthop(entries): + # Change 'gateway' entry to 'nexthop' + changed_entries = copy.deepcopy(entries) + for entry in changed_entries: + entry['nexthop'] = entry['gateway'] + del entry['gateway'] + + return changed_entries + + +def convert_entries_to_gateway(entries): + # Change 'nexthop' entry to 'gateway' + changed_entries = copy.deepcopy(entries) + for entry in changed_entries: + entry['gateway'] = entry['nexthop'] + del entry['nexthop'] + + return changed_entries + + +def _get_attrs(client_manager, parsed_args, is_create=True): + attrs = {} + if 'name' in parsed_args and parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + + if is_create: + if 'project' in parsed_args and parsed_args.project is not None: + 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 + client = client_manager.network + attrs['network_id'] = client.find_network(parsed_args.network, + ignore_missing=False).id + if parsed_args.subnet_pool is not None: + subnet_pool = client.find_subnet_pool(parsed_args.subnet_pool, + ignore_missing=False) + attrs['subnetpool_id'] = subnet_pool.id + if parsed_args.use_default_subnet_pool: + attrs['use_default_subnetpool'] = True + if parsed_args.prefix_length is not None: + attrs['prefixlen'] = parsed_args.prefix_length + if parsed_args.subnet_range is not None: + attrs['cidr'] = parsed_args.subnet_range + if parsed_args.ip_version is not None: + attrs['ip_version'] = parsed_args.ip_version + if parsed_args.ipv6_ra_mode is not None: + attrs['ipv6_ra_mode'] = parsed_args.ipv6_ra_mode + if parsed_args.ipv6_address_mode is not None: + attrs['ipv6_address_mode'] = parsed_args.ipv6_address_mode + + if 'gateway' in parsed_args and parsed_args.gateway is not None: + gateway = parsed_args.gateway.lower() + + if not is_create and gateway == 'auto': + raise exceptions.CommandError("Auto option is not available" + " for Subnet Set. Valid options are" + " <ip-address> or none") + elif gateway != 'auto': + if gateway == 'none': + attrs['gateway_ip'] = None + else: + attrs['gateway_ip'] = gateway + if ('allocation_pools' in parsed_args and + parsed_args.allocation_pools is not None): + attrs['allocation_pools'] = parsed_args.allocation_pools + if parsed_args.dhcp: + attrs['enable_dhcp'] = True + elif parsed_args.no_dhcp: + attrs['enable_dhcp'] = False + if ('dns_nameservers' in parsed_args and + parsed_args.dns_nameservers is not None): + attrs['dns_nameservers'] = parsed_args.dns_nameservers + if 'host_routes' in parsed_args and parsed_args.host_routes is not None: + # Change 'gateway' entry to 'nexthop' to match the API + attrs['host_routes'] = convert_entries_to_nexthop( + parsed_args.host_routes) + return attrs + + +class CreateSubnet(command.ShowOne): + """Create a subnet""" + + def get_parser(self, prog_name): + parser = super(CreateSubnet, self).get_parser(prog_name) + parser.add_argument( + 'name', + help='New subnet name', + ) + parser.add_argument( + '--project', + metavar='<project>', + help="Owner's project (name or ID)", + ) + identity_common.add_project_domain_option_to_parser(parser) + subnet_pool_group = parser.add_mutually_exclusive_group() + subnet_pool_group.add_argument( + '--subnet-pool', + metavar='<subnet-pool>', + help='Subnet pool from which this subnet will obtain a CIDR ' + '(Name or ID)', + ) + subnet_pool_group.add_argument( + '--use-default-subnet-pool', + action='store_true', + help='Use default subnet pool for --ip-version', + ) + parser.add_argument( + '--prefix-length', + metavar='<prefix-length>', + help='Prefix length for subnet allocation from subnetpool', + ) + parser.add_argument( + '--subnet-range', + metavar='<subnet-range>', + help='Subnet range in CIDR notation ' + '(required if --subnet-pool is not specified, ' + 'optional otherwise)', + ) + dhcp_enable_group = parser.add_mutually_exclusive_group() + dhcp_enable_group.add_argument( + '--dhcp', + action='store_true', + default=True, + help='Enable DHCP (default)', + ) + dhcp_enable_group.add_argument( + '--no-dhcp', + action='store_true', + help='Disable DHCP', + ) + parser.add_argument( + '--gateway', + metavar='<gateway>', + default='auto', + help="Specify a gateway for the subnet. The three options are: " + " <ip-address>: Specific IP address to use as the gateway " + " 'auto': Gateway address should automatically be " + " chosen from within the subnet itself " + " 'none': This subnet will not use a gateway " + "e.g.: --gateway 192.168.9.1, --gateway auto, --gateway none" + "(default is 'auto')", + ) + parser.add_argument( + '--ip-version', + type=int, + default=4, + choices=[4, 6], + help='IP version (default is 4). Note that when subnet pool is ' + 'specified, IP version is determined from the subnet pool ' + 'and this option is ignored.', + ) + parser.add_argument( + '--ipv6-ra-mode', + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help='IPv6 RA (Router Advertisement) mode, ' + 'valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]', + ) + parser.add_argument( + '--ipv6-address-mode', + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help='IPv6 address mode, ' + 'valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]', + ) + parser.add_argument( + '--network', + required=True, + metavar='<network>', + help='Network this subnet belongs to (name or ID)', + ) + _get_common_parse_arguments(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_subnet(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + return (columns, data) + + class DeleteSubnet(command.Command): """Delete subnet""" @@ -46,7 +277,7 @@ class DeleteSubnet(command.Command): parser.add_argument( 'subnet', metavar="<subnet>", - help="Subnet to delete (name or ID)" + help="Subnet to delete (name or ID)", ) return parser @@ -89,6 +320,56 @@ class ListSubnet(command.Lister): ) for s in data)) +class SetSubnet(command.Command): + """Set subnet properties""" + + def get_parser(self, prog_name): + parser = super(SetSubnet, self).get_parser(prog_name) + parser.add_argument( + 'subnet', + metavar="<subnet>", + help=("Subnet to modify (name or ID)") + ) + parser.add_argument( + '--name', + metavar='<name>', + help='Updated name of the subnet', + ) + dhcp_enable_group = parser.add_mutually_exclusive_group() + dhcp_enable_group.add_argument( + '--dhcp', + action='store_true', + default=None, + help='Enable DHCP', + ) + dhcp_enable_group.add_argument( + '--no-dhcp', + action='store_true', + help='Disable DHCP', + ) + parser.add_argument( + '--gateway', + metavar='<gateway>', + help="Specify a gateway for the subnet. The options are: " + " <ip-address>: Specific IP address to use as the gateway " + " 'none': This subnet will not use a gateway " + "e.g.: --gateway 192.168.9.1, --gateway none" + ) + _get_common_parse_arguments(parser) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_subnet(parsed_args.subnet, ignore_missing=False) + attrs = _get_attrs(self.app.client_manager, parsed_args, + is_create=False) + if not attrs: + msg = "Nothing specified to be set" + raise exceptions.CommandError(msg) + client.update_subnet(obj, **attrs) + return + + class ShowSubnet(command.ShowOne): """Show subnet details""" @@ -97,7 +378,7 @@ class ShowSubnet(command.ShowOne): parser.add_argument( 'subnet', metavar="<subnet>", - help="Subnet to show (name or ID)" + help="Subnet to show (name or ID)", ) return parser diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 5bb45c12..6b6fc090 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -14,11 +14,14 @@ """Subnet pool action implementations""" from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common as identity_common def _get_columns(item): - columns = item.keys() + columns = list(item.keys()) if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') @@ -30,6 +33,93 @@ _formatters = { } +def _get_attrs(client_manager, parsed_args): + attrs = {} + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if parsed_args.prefixes is not None: + attrs['prefixes'] = parsed_args.prefixes + if parsed_args.default_prefix_length is not None: + attrs['default_prefixlen'] = parsed_args.default_prefix_length + if parsed_args.min_prefix_length is not None: + attrs['min_prefixlen'] = parsed_args.min_prefix_length + if parsed_args.max_prefix_length is not None: + attrs['max_prefixlen'] = parsed_args.max_prefix_length + + # "subnet pool set" command doesn't support setting project. + if 'project' in parsed_args and parsed_args.project is not None: + 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 _add_prefix_options(parser): + parser.add_argument( + '--pool-prefix', + metavar='<pool-prefix>', + dest='prefixes', + action='append', + help='Set subnet pool prefixes (in CIDR notation). ' + 'Repeat this option to set multiple prefixes.', + ) + parser.add_argument( + '--default-prefix-length', + metavar='<default-prefix-length>', + action=parseractions.NonNegativeAction, + help='Set subnet pool default prefix length', + ) + parser.add_argument( + '--min-prefix-length', + metavar='<min-prefix-length>', + action=parseractions.NonNegativeAction, + help='Set subnet pool minimum prefix length', + ) + parser.add_argument( + '--max-prefix-length', + metavar='<max-prefix-length>', + action=parseractions.NonNegativeAction, + help='Set subnet pool maximum prefix length', + ) + + +class CreateSubnetPool(command.ShowOne): + """Create subnet pool""" + + def get_parser(self, prog_name): + parser = super(CreateSubnetPool, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='<name>', + help='Name of the new subnet pool' + ) + _add_prefix_options(parser) + parser.add_argument( + '--project', + metavar='<project>', + help="Owner's project (name or ID)", + ) + identity_common.add_project_domain_option_to_parser(parser) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + # NeutronServer expects prefixes to be a List + if "prefixes" not in attrs: + attrs['prefixes'] = [] + obj = client.create_subnet_pool(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + return (columns, data) + + class DeleteSubnetPool(command.Command): """Delete subnet pool""" @@ -37,8 +127,8 @@ class DeleteSubnetPool(command.Command): parser = super(DeleteSubnetPool, self).get_parser(prog_name) parser.add_argument( 'subnet_pool', - metavar="<subnet-pool>", - help=("Subnet pool to delete (name or ID)") + metavar='<subnet-pool>', + help='Subnet pool to delete (name or ID)' ) return parser @@ -98,6 +188,42 @@ class ListSubnetPool(command.Lister): ) for s in data)) +class SetSubnetPool(command.Command): + """Set subnet pool properties""" + + def get_parser(self, prog_name): + parser = super(SetSubnetPool, self).get_parser(prog_name) + parser.add_argument( + 'subnet_pool', + metavar='<subnet-pool>', + help='Subnet pool to modify (name or ID)' + ) + parser.add_argument( + '--name', + metavar='<name>', + help='Set subnet pool name', + ) + _add_prefix_options(parser) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_subnet_pool(parsed_args.subnet_pool, + ignore_missing=False) + + attrs = _get_attrs(self.app.client_manager, parsed_args) + if attrs == {}: + msg = "Nothing specified to be set" + raise exceptions.CommandError(msg) + + # Existing prefixes must be a subset of the new prefixes. + if 'prefixes' in attrs: + attrs['prefixes'].extend(obj.prefixes) + + client.update_subnet_pool(obj, **attrs) + + class ShowSubnetPool(command.ShowOne): """Display subnet pool details""" @@ -105,8 +231,8 @@ class ShowSubnetPool(command.ShowOne): parser = super(ShowSubnetPool, self).get_parser(prog_name) parser.add_argument( 'subnet_pool', - metavar="<subnet-pool>", - help=("Subnet pool to display (name or ID)") + metavar='<subnet-pool>', + help='Subnet pool to display (name or ID)' ) return parser |
