1#   Licensed under the Apache License, Version 2.0 (the "License"); you may
2#   not use this file except in compliance with the License. You may obtain
3#   a copy of the License at
4#
5#        http://www.apache.org/licenses/LICENSE-2.0
6#
7#   Unless required by applicable law or agreed to in writing, software
8#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10#   License for the specific language governing permissions and limitations
11#   under the License.
12#
13
14"""Subnet action implementations"""
15
16import copy
17import logging
18
19from cliff import columns as cliff_columns
20from osc_lib.cli import format_columns
21from osc_lib.cli import parseractions
22from osc_lib.command import command
23from osc_lib import exceptions
24from osc_lib import utils
25from osc_lib.utils import tags as _tag
26
27from openstackclient.i18n import _
28from openstackclient.identity import common as identity_common
29from openstackclient.network import sdk_utils
30
31
32LOG = logging.getLogger(__name__)
33
34
35def _update_arguments(obj_list, parsed_args_list, option):
36    for item in parsed_args_list:
37        try:
38            obj_list.remove(item)
39        except ValueError:
40            msg = (_("Subnet does not contain %(option)s %(value)s") %
41                   {'option': option, 'value': item})
42            raise exceptions.CommandError(msg)
43
44
45class AllocationPoolsColumn(cliff_columns.FormattableColumn):
46    def human_readable(self):
47        pool_formatted = ['%s-%s' % (pool.get('start', ''),
48                                     pool.get('end', ''))
49                          for pool in self._value]
50        return ','.join(pool_formatted)
51
52
53class HostRoutesColumn(cliff_columns.FormattableColumn):
54    def human_readable(self):
55        # Map the host route keys to match --host-route option.
56        return utils.format_list_of_dicts(
57            convert_entries_to_gateway(self._value))
58
59
60_formatters = {
61    'allocation_pools': AllocationPoolsColumn,
62    'dns_nameservers': format_columns.ListColumn,
63    'host_routes': HostRoutesColumn,
64    'location': format_columns.DictColumn,
65    'service_types': format_columns.ListColumn,
66    'tags': format_columns.ListColumn,
67}
68
69
70def _get_common_parse_arguments(parser, is_create=True):
71    parser.add_argument(
72        '--allocation-pool',
73        metavar='start=<ip-address>,end=<ip-address>',
74        dest='allocation_pools',
75        action=parseractions.MultiKeyValueAction,
76        required_keys=['start', 'end'],
77        help=_("Allocation pool IP addresses for this subnet "
78               "e.g.: start=192.168.199.2,end=192.168.199.254 "
79               "(repeat option to add multiple IP addresses)")
80    )
81    if not is_create:
82        parser.add_argument(
83            '--no-allocation-pool',
84            action='store_true',
85            help=_("Clear associated allocation-pools from the subnet. "
86                   "Specify both --allocation-pool and --no-allocation-pool "
87                   "to overwrite the current allocation pool information.")
88        )
89    parser.add_argument(
90        '--dns-nameserver',
91        metavar='<dns-nameserver>',
92        action='append',
93        dest='dns_nameservers',
94        help=_("DNS server for this subnet "
95               "(repeat option to set multiple DNS servers)")
96    )
97
98    if not is_create:
99        parser.add_argument(
100            '--no-dns-nameservers',
101            action='store_true',
102            help=_("Clear existing information of DNS Nameservers. "
103                   "Specify both --dns-nameserver and --no-dns-nameserver "
104                   "to overwrite the current DNS Nameserver information.")
105        )
106    parser.add_argument(
107        '--host-route',
108        metavar='destination=<subnet>,gateway=<ip-address>',
109        dest='host_routes',
110        action=parseractions.MultiKeyValueAction,
111        required_keys=['destination', 'gateway'],
112        help=_("Additional route for this subnet "
113               "e.g.: destination=10.10.0.0/16,gateway=192.168.71.254 "
114               "destination: destination subnet (in CIDR notation) "
115               "gateway: nexthop IP address "
116               "(repeat option to add multiple routes)")
117    )
118    if not is_create:
119        parser.add_argument(
120            '--no-host-route',
121            action='store_true',
122            help=_("Clear associated host-routes from the subnet. "
123                   "Specify both --host-route and --no-host-route "
124                   "to overwrite the current host route information.")
125        )
126    parser.add_argument(
127        '--service-type',
128        metavar='<service-type>',
129        action='append',
130        dest='service_types',
131        help=_("Service type for this subnet "
132               "e.g.: network:floatingip_agent_gateway. "
133               "Must be a valid device owner value for a network port "
134               "(repeat option to set multiple service types)")
135    )
136
137
138def _get_columns(item):
139    column_map = {
140        'is_dhcp_enabled': 'enable_dhcp',
141        'subnet_pool_id': 'subnetpool_id',
142        'tenant_id': 'project_id',
143    }
144    # Do not show this column when displaying a subnet
145    invisible_columns = ['use_default_subnet_pool']
146    return sdk_utils.get_osc_show_columns_for_sdk_resource(
147        item,
148        column_map,
149        invisible_columns=invisible_columns
150    )
151
152
153def convert_entries_to_nexthop(entries):
154    # Change 'gateway' entry to 'nexthop'
155    changed_entries = copy.deepcopy(entries)
156    for entry in changed_entries:
157        if 'gateway' in entry:
158            entry['nexthop'] = entry['gateway']
159            del entry['gateway']
160
161    return changed_entries
162
163
164def convert_entries_to_gateway(entries):
165    # Change 'nexthop' entry to 'gateway'
166    changed_entries = copy.deepcopy(entries)
167    for entry in changed_entries:
168        if 'nexthop' in entry:
169            entry['gateway'] = entry['nexthop']
170            del entry['nexthop']
171
172    return changed_entries
173
174
175def _get_attrs(client_manager, parsed_args, is_create=True):
176    attrs = {}
177    client = client_manager.network
178    if 'name' in parsed_args and parsed_args.name is not None:
179        attrs['name'] = parsed_args.name
180
181    if is_create:
182        if 'project' in parsed_args and parsed_args.project is not None:
183            identity_client = client_manager.identity
184            project_id = identity_common.find_project(
185                identity_client,
186                parsed_args.project,
187                parsed_args.project_domain,
188            ).id
189            attrs['tenant_id'] = project_id
190        attrs['network_id'] = client.find_network(parsed_args.network,
191                                                  ignore_missing=False).id
192        if parsed_args.subnet_pool is not None:
193            subnet_pool = client.find_subnet_pool(parsed_args.subnet_pool,
194                                                  ignore_missing=False)
195            attrs['subnetpool_id'] = subnet_pool.id
196        if parsed_args.use_prefix_delegation:
197            attrs['subnetpool_id'] = "prefix_delegation"
198        if parsed_args.use_default_subnet_pool:
199            attrs['use_default_subnet_pool'] = True
200        if parsed_args.prefix_length is not None:
201            attrs['prefixlen'] = parsed_args.prefix_length
202        if parsed_args.subnet_range is not None:
203            attrs['cidr'] = parsed_args.subnet_range
204        if parsed_args.ip_version is not None:
205            attrs['ip_version'] = parsed_args.ip_version
206        if parsed_args.ipv6_ra_mode is not None:
207            attrs['ipv6_ra_mode'] = parsed_args.ipv6_ra_mode
208        if parsed_args.ipv6_address_mode is not None:
209            attrs['ipv6_address_mode'] = parsed_args.ipv6_address_mode
210
211    if parsed_args.network_segment is not None:
212        attrs['segment_id'] = client.find_segment(
213            parsed_args.network_segment, ignore_missing=False).id
214    if 'gateway' in parsed_args and parsed_args.gateway is not None:
215        gateway = parsed_args.gateway.lower()
216
217        if not is_create and gateway == 'auto':
218            msg = _("Auto option is not available for Subnet Set. "
219                    "Valid options are <ip-address> or none")
220            raise exceptions.CommandError(msg)
221        elif gateway != 'auto':
222            if gateway == 'none':
223                attrs['gateway_ip'] = None
224            else:
225                attrs['gateway_ip'] = gateway
226    if ('allocation_pools' in parsed_args and
227       parsed_args.allocation_pools is not None):
228        attrs['allocation_pools'] = parsed_args.allocation_pools
229    if parsed_args.dhcp:
230        attrs['enable_dhcp'] = True
231    if parsed_args.no_dhcp:
232        attrs['enable_dhcp'] = False
233    if parsed_args.dns_publish_fixed_ip:
234        attrs['dns_publish_fixed_ip'] = True
235    if parsed_args.no_dns_publish_fixed_ip:
236        attrs['dns_publish_fixed_ip'] = False
237    if ('dns_nameservers' in parsed_args and
238       parsed_args.dns_nameservers is not None):
239        attrs['dns_nameservers'] = parsed_args.dns_nameservers
240    if 'host_routes' in parsed_args and parsed_args.host_routes is not None:
241        # Change 'gateway' entry to 'nexthop' to match the API
242        attrs['host_routes'] = convert_entries_to_nexthop(
243            parsed_args.host_routes)
244    if ('service_types' in parsed_args and
245       parsed_args.service_types is not None):
246        attrs['service_types'] = parsed_args.service_types
247    if parsed_args.description is not None:
248        attrs['description'] = parsed_args.description
249    return attrs
250
251
252# TODO(abhiraut): Use the SDK resource mapped attribute names once the
253# OSC minimum requirements include SDK 1.0.
254class CreateSubnet(command.ShowOne):
255    _description = _("Create a subnet")
256
257    def get_parser(self, prog_name):
258        parser = super(CreateSubnet, self).get_parser(prog_name)
259        parser.add_argument(
260            'name',
261            metavar='<name>',
262            help=_("New subnet name")
263        )
264        parser.add_argument(
265            '--project',
266            metavar='<project>',
267            help=_("Owner's project (name or ID)")
268        )
269        identity_common.add_project_domain_option_to_parser(parser)
270        subnet_pool_group = parser.add_mutually_exclusive_group()
271        subnet_pool_group.add_argument(
272            '--subnet-pool',
273            metavar='<subnet-pool>',
274            help=_("Subnet pool from which this subnet will obtain a CIDR "
275                   "(Name or ID)")
276        )
277        subnet_pool_group.add_argument(
278            '--use-prefix-delegation',
279            help=_("Use 'prefix-delegation' if IP is IPv6 format "
280                   "and IP would be delegated externally")
281        )
282        subnet_pool_group.add_argument(
283            '--use-default-subnet-pool',
284            action='store_true',
285            help=_("Use default subnet pool for --ip-version")
286        )
287        parser.add_argument(
288            '--prefix-length',
289            metavar='<prefix-length>',
290            help=_("Prefix length for subnet allocation from subnet pool")
291        )
292        parser.add_argument(
293            '--subnet-range',
294            metavar='<subnet-range>',
295            help=_("Subnet range in CIDR notation "
296                   "(required if --subnet-pool is not specified, "
297                   "optional otherwise)")
298        )
299        dhcp_enable_group = parser.add_mutually_exclusive_group()
300        dhcp_enable_group.add_argument(
301            '--dhcp',
302            action='store_true',
303            help=_("Enable DHCP (default)")
304        )
305        dhcp_enable_group.add_argument(
306            '--no-dhcp',
307            action='store_true',
308            help=_("Disable DHCP")
309        )
310        dns_publish_fixed_ip_group = parser.add_mutually_exclusive_group()
311        dns_publish_fixed_ip_group.add_argument(
312            '--dns-publish-fixed-ip',
313            action='store_true',
314            help=_("Enable publishing fixed IPs in DNS")
315        )
316        dns_publish_fixed_ip_group.add_argument(
317            '--no-dns-publish-fixed-ip',
318            action='store_true',
319            help=_("Disable publishing fixed IPs in DNS (default)")
320        )
321        parser.add_argument(
322            '--gateway',
323            metavar='<gateway>',
324            default='auto',
325            help=_("Specify a gateway for the subnet.  The three options are: "
326                   "<ip-address>: Specific IP address to use as the gateway, "
327                   "'auto': Gateway address should automatically be chosen "
328                   "from within the subnet itself, 'none': This subnet will "
329                   "not use a gateway, e.g.: --gateway 192.168.9.1, "
330                   "--gateway auto, --gateway none (default is 'auto').")
331        )
332        parser.add_argument(
333            '--ip-version',
334            type=int,
335            default=4,
336            choices=[4, 6],
337            help=_("IP version (default is 4).  Note that when subnet pool is "
338                   "specified, IP version is determined from the subnet pool "
339                   "and this option is ignored.")
340        )
341        parser.add_argument(
342            '--ipv6-ra-mode',
343            choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'],
344            help=_("IPv6 RA (Router Advertisement) mode, "
345                   "valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]")
346        )
347        parser.add_argument(
348            '--ipv6-address-mode',
349            choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'],
350            help=_("IPv6 address mode, "
351                   "valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]")
352        )
353        parser.add_argument(
354            '--network-segment',
355            metavar='<network-segment>',
356            help=_("Network segment to associate with this subnet "
357                   "(name or ID)")
358        )
359        parser.add_argument(
360            '--network',
361            required=True,
362            metavar='<network>',
363            help=_("Network this subnet belongs to (name or ID)")
364        )
365        parser.add_argument(
366            '--description',
367            metavar='<description>',
368            help=_("Set subnet description")
369        )
370        _get_common_parse_arguments(parser)
371        _tag.add_tag_option_to_parser_for_create(parser, _('subnet'))
372        return parser
373
374    def take_action(self, parsed_args):
375        client = self.app.client_manager.network
376        attrs = _get_attrs(self.app.client_manager, parsed_args)
377        obj = client.create_subnet(**attrs)
378        # tags cannot be set when created, so tags need to be set later.
379        _tag.update_tags_for_set(client, obj, parsed_args)
380        display_columns, columns = _get_columns(obj)
381        data = utils.get_item_properties(obj, columns, formatters=_formatters)
382        return (display_columns, data)
383
384
385class DeleteSubnet(command.Command):
386    _description = _("Delete subnet(s)")
387
388    def get_parser(self, prog_name):
389        parser = super(DeleteSubnet, self).get_parser(prog_name)
390        parser.add_argument(
391            'subnet',
392            metavar="<subnet>",
393            nargs='+',
394            help=_("Subnet(s) to delete (name or ID)")
395        )
396        return parser
397
398    def take_action(self, parsed_args):
399        client = self.app.client_manager.network
400        result = 0
401
402        for subnet in parsed_args.subnet:
403            try:
404                obj = client.find_subnet(subnet, ignore_missing=False)
405                client.delete_subnet(obj)
406            except Exception as e:
407                result += 1
408                LOG.error(_("Failed to delete subnet with "
409                          "name or ID '%(subnet)s': %(e)s"),
410                          {'subnet': subnet, 'e': e})
411
412        if result > 0:
413            total = len(parsed_args.subnet)
414            msg = (_("%(result)s of %(total)s subnets failed "
415                   "to delete.") % {'result': result, 'total': total})
416            raise exceptions.CommandError(msg)
417
418
419# TODO(abhiraut): Use only the SDK resource mapped attribute names once the
420# OSC minimum requirements include SDK 1.0.
421class ListSubnet(command.Lister):
422    _description = _("List subnets")
423
424    def get_parser(self, prog_name):
425        parser = super(ListSubnet, self).get_parser(prog_name)
426        parser.add_argument(
427            '--long',
428            action='store_true',
429            default=False,
430            help=_("List additional fields in output")
431        )
432        parser.add_argument(
433            '--ip-version',
434            type=int,
435            choices=[4, 6],
436            metavar='<ip-version>',
437            dest='ip_version',
438            help=_("List only subnets of given IP version in output. "
439                   "Allowed values for IP version are 4 and 6."),
440        )
441        dhcp_enable_group = parser.add_mutually_exclusive_group()
442        dhcp_enable_group.add_argument(
443            '--dhcp',
444            action='store_true',
445            help=_("List subnets which have DHCP enabled")
446        )
447        dhcp_enable_group.add_argument(
448            '--no-dhcp',
449            action='store_true',
450            help=_("List subnets which have DHCP disabled")
451        )
452        parser.add_argument(
453            '--service-type',
454            metavar='<service-type>',
455            action='append',
456            dest='service_types',
457            help=_("List only subnets of a given service type in output "
458                   "e.g.: network:floatingip_agent_gateway. "
459                   "Must be a valid device owner value for a network port "
460                   "(repeat option to list multiple service types)")
461        )
462        parser.add_argument(
463            '--project',
464            metavar='<project>',
465            help=_("List only subnets which belong to a given project "
466                   "in output (name or ID)")
467        )
468        identity_common.add_project_domain_option_to_parser(parser)
469        parser.add_argument(
470            '--network',
471            metavar='<network>',
472            help=_("List only subnets which belong to a given network "
473                   "in output (name or ID)")
474        )
475        parser.add_argument(
476            '--gateway',
477            metavar='<gateway>',
478            help=_("List only subnets of given gateway IP in output")
479        )
480        parser.add_argument(
481            '--name',
482            metavar='<name>',
483            help=_("List only subnets of given name in output")
484        )
485        parser.add_argument(
486            '--subnet-range',
487            metavar='<subnet-range>',
488            help=_("List only subnets of given subnet range "
489                   "(in CIDR notation) in output "
490                   "e.g.: --subnet-range 10.10.0.0/16")
491        )
492        _tag.add_tag_filtering_option_to_parser(parser, _('subnets'))
493        return parser
494
495    def take_action(self, parsed_args):
496        identity_client = self.app.client_manager.identity
497        network_client = self.app.client_manager.network
498        filters = {}
499        if parsed_args.ip_version:
500            filters['ip_version'] = parsed_args.ip_version
501        if parsed_args.dhcp:
502            filters['enable_dhcp'] = True
503            filters['is_dhcp_enabled'] = True
504        elif parsed_args.no_dhcp:
505            filters['enable_dhcp'] = False
506            filters['is_dhcp_enabled'] = False
507        if parsed_args.service_types:
508            filters['service_types'] = parsed_args.service_types
509        if parsed_args.project:
510            project_id = identity_common.find_project(
511                identity_client,
512                parsed_args.project,
513                parsed_args.project_domain,
514            ).id
515            filters['tenant_id'] = project_id
516            filters['project_id'] = project_id
517        if parsed_args.network:
518            network_id = network_client.find_network(parsed_args.network,
519                                                     ignore_missing=False).id
520            filters['network_id'] = network_id
521        if parsed_args.gateway:
522            filters['gateway_ip'] = parsed_args.gateway
523        if parsed_args.name:
524            filters['name'] = parsed_args.name
525        if parsed_args.subnet_range:
526            filters['cidr'] = parsed_args.subnet_range
527        _tag.get_tag_filtering_args(parsed_args, filters)
528        data = network_client.subnets(**filters)
529
530        headers = ('ID', 'Name', 'Network', 'Subnet')
531        columns = ('id', 'name', 'network_id', 'cidr')
532        if parsed_args.long:
533            headers += ('Project', 'DHCP', 'Name Servers',
534                        'Allocation Pools', 'Host Routes', 'IP Version',
535                        'Gateway', 'Service Types', 'Tags')
536            columns += ('project_id', 'is_dhcp_enabled', 'dns_nameservers',
537                        'allocation_pools', 'host_routes', 'ip_version',
538                        'gateway_ip', 'service_types', 'tags')
539
540        return (headers,
541                (utils.get_item_properties(
542                    s, columns,
543                    formatters=_formatters,
544                ) for s in data))
545
546
547# TODO(abhiraut): Use the SDK resource mapped attribute names once the
548# OSC minimum requirements include SDK 1.0.
549class SetSubnet(command.Command):
550    _description = _("Set subnet properties")
551
552    def get_parser(self, prog_name):
553        parser = super(SetSubnet, self).get_parser(prog_name)
554        parser.add_argument(
555            'subnet',
556            metavar="<subnet>",
557            help=_("Subnet to modify (name or ID)")
558        )
559        parser.add_argument(
560            '--name',
561            metavar='<name>',
562            help=_("Updated name of the subnet")
563        )
564        dhcp_enable_group = parser.add_mutually_exclusive_group()
565        dhcp_enable_group.add_argument(
566            '--dhcp',
567            action='store_true',
568            default=None,
569            help=_("Enable DHCP")
570        )
571        dhcp_enable_group.add_argument(
572            '--no-dhcp',
573            action='store_true',
574            help=_("Disable DHCP")
575        )
576        dns_publish_fixed_ip_group = parser.add_mutually_exclusive_group()
577        dns_publish_fixed_ip_group.add_argument(
578            '--dns-publish-fixed-ip',
579            action='store_true',
580            help=_("Enable publishing fixed IPs in DNS")
581        )
582        dns_publish_fixed_ip_group.add_argument(
583            '--no-dns-publish-fixed-ip',
584            action='store_true',
585            help=_("Disable publishing fixed IPs in DNS")
586        )
587        parser.add_argument(
588            '--gateway',
589            metavar='<gateway>',
590            help=_("Specify a gateway for the subnet. The options are: "
591                   "<ip-address>: Specific IP address to use as the gateway, "
592                   "'none': This subnet will not use a gateway, "
593                   "e.g.: --gateway 192.168.9.1, --gateway none.")
594        )
595        parser.add_argument(
596            '--network-segment',
597            metavar='<network-segment>',
598            help=_("Network segment to associate with this subnet (name or "
599                   "ID). It is only allowed to set the segment if the current "
600                   "value is `None`, the network must also have only one "
601                   "segment and only one subnet can exist on the network.")
602        )
603        parser.add_argument(
604            '--description',
605            metavar='<description>',
606            help=_("Set subnet description")
607        )
608        _tag.add_tag_option_to_parser_for_set(parser, _('subnet'))
609        _get_common_parse_arguments(parser, is_create=False)
610        return parser
611
612    def take_action(self, parsed_args):
613        client = self.app.client_manager.network
614        obj = client.find_subnet(parsed_args.subnet, ignore_missing=False)
615        attrs = _get_attrs(self.app.client_manager, parsed_args,
616                           is_create=False)
617        if 'dns_nameservers' in attrs:
618            if not parsed_args.no_dns_nameservers:
619                attrs['dns_nameservers'] += obj.dns_nameservers
620        elif parsed_args.no_dns_nameservers:
621            attrs['dns_nameservers'] = []
622        if 'host_routes' in attrs:
623            if not parsed_args.no_host_route:
624                attrs['host_routes'] += obj.host_routes
625        elif parsed_args.no_host_route:
626            attrs['host_routes'] = []
627        if 'allocation_pools' in attrs:
628            if not parsed_args.no_allocation_pool:
629                attrs['allocation_pools'] += obj.allocation_pools
630        elif parsed_args.no_allocation_pool:
631            attrs['allocation_pools'] = []
632        if 'service_types' in attrs:
633            attrs['service_types'] += obj.service_types
634        if attrs:
635            client.update_subnet(obj, **attrs)
636        # tags is a subresource and it needs to be updated separately.
637        _tag.update_tags_for_set(client, obj, parsed_args)
638        return
639
640
641class ShowSubnet(command.ShowOne):
642    _description = _("Display subnet details")
643
644    def get_parser(self, prog_name):
645        parser = super(ShowSubnet, self).get_parser(prog_name)
646        parser.add_argument(
647            'subnet',
648            metavar="<subnet>",
649            help=_("Subnet to display (name or ID)")
650        )
651        return parser
652
653    def take_action(self, parsed_args):
654        obj = self.app.client_manager.network.find_subnet(parsed_args.subnet,
655                                                          ignore_missing=False)
656        display_columns, columns = _get_columns(obj)
657        data = utils.get_item_properties(obj, columns, formatters=_formatters)
658        return (display_columns, data)
659
660
661class UnsetSubnet(command.Command):
662    _description = _("Unset subnet properties")
663
664    def get_parser(self, prog_name):
665        parser = super(UnsetSubnet, self).get_parser(prog_name)
666        parser.add_argument(
667            '--allocation-pool',
668            metavar='start=<ip-address>,end=<ip-address>',
669            dest='allocation_pools',
670            action=parseractions.MultiKeyValueAction,
671            required_keys=['start', 'end'],
672            help=_('Allocation pool IP addresses to be removed from this '
673                   'subnet e.g.: start=192.168.199.2,end=192.168.199.254 '
674                   '(repeat option to unset multiple allocation pools)')
675        )
676        parser.add_argument(
677            '--dns-nameserver',
678            metavar='<dns-nameserver>',
679            action='append',
680            dest='dns_nameservers',
681            help=_('DNS server to be removed from this subnet '
682                   '(repeat option to unset multiple DNS servers)')
683        )
684        parser.add_argument(
685            '--host-route',
686            metavar='destination=<subnet>,gateway=<ip-address>',
687            dest='host_routes',
688            action=parseractions.MultiKeyValueAction,
689            required_keys=['destination', 'gateway'],
690            help=_('Route to be removed from this subnet '
691                   'e.g.: destination=10.10.0.0/16,gateway=192.168.71.254 '
692                   'destination: destination subnet (in CIDR notation) '
693                   'gateway: nexthop IP address '
694                   '(repeat option to unset multiple host routes)')
695        )
696        parser.add_argument(
697            '--service-type',
698            metavar='<service-type>',
699            action='append',
700            dest='service_types',
701            help=_('Service type to be removed from this subnet '
702                   'e.g.: network:floatingip_agent_gateway. '
703                   'Must be a valid device owner value for a network port '
704                   '(repeat option to unset multiple service types)')
705        )
706        _tag.add_tag_option_to_parser_for_unset(parser, _('subnet'))
707        parser.add_argument(
708            'subnet',
709            metavar="<subnet>",
710            help=_("Subnet to modify (name or ID)")
711        )
712        return parser
713
714    def take_action(self, parsed_args):
715        client = self.app.client_manager.network
716        obj = client.find_subnet(parsed_args.subnet, ignore_missing=False)
717
718        attrs = {}
719        if parsed_args.dns_nameservers:
720            attrs['dns_nameservers'] = copy.deepcopy(obj.dns_nameservers)
721            _update_arguments(attrs['dns_nameservers'],
722                              parsed_args.dns_nameservers,
723                              'dns-nameserver')
724        if parsed_args.host_routes:
725            attrs['host_routes'] = copy.deepcopy(obj.host_routes)
726            _update_arguments(
727                attrs['host_routes'],
728                convert_entries_to_nexthop(parsed_args.host_routes),
729                'host-route')
730        if parsed_args.allocation_pools:
731            attrs['allocation_pools'] = copy.deepcopy(obj.allocation_pools)
732            _update_arguments(attrs['allocation_pools'],
733                              parsed_args.allocation_pools,
734                              'allocation-pool')
735
736        if parsed_args.service_types:
737            attrs['service_types'] = copy.deepcopy(obj.service_types)
738            _update_arguments(attrs['service_types'],
739                              parsed_args.service_types,
740                              'service-type')
741        if attrs:
742            client.update_subnet(obj, **attrs)
743
744        # tags is a subresource and it needs to be updated separately.
745        _tag.update_tags_for_unset(client, obj, parsed_args)
746