Skip to content

Commit 71b8919

Browse files
committed
Add "os subnet create" command using SDK
Implement the openstack client subnet create command using SDK calls. Co-Authored-By: Terry Howe <terrylhowe@gmail.com> Partially implements: blueprint neutron-client Closes-Bug: #1542364 Change-Id: Ia6120b8dccf2ee83dc89b3f496f7180d4dc5199a
1 parent 4bb48c0 commit 71b8919

6 files changed

Lines changed: 671 additions & 7 deletions

File tree

doc/source/command-objects/subnet.rst

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,114 @@ Delete a subnet
2020

2121
Subnet to delete (name or ID)
2222

23+
subnet create
24+
--------------
25+
26+
Create new subnet
27+
28+
.. program:: subnet create
29+
.. code:: bash
30+
31+
os subnet create
32+
[--project <project> [--project-domain <project-domain>]]
33+
[--subnet-pool <subnet-pool> | --use-default-subnet-pool [--prefix-length <prefix-length>]]
34+
[--subnet-range <subnet-range>]
35+
[--allocation-pool start=<ip-address>,end=<ip-address>]
36+
[--dhcp | --no-dhcp]
37+
[--dns-nameserver <dns-nameserver>]
38+
[--gateway <gateway>]
39+
[--host-route destination=<subnet>,gateway=<ip-address>]
40+
[--ip-version {4,6}]
41+
[--ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}]
42+
[--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}]
43+
--network <network>
44+
<name>
45+
46+
.. option:: --project <project>
47+
48+
Owner's project (name or ID)
49+
50+
.. option:: --project-domain <project-domain>
51+
52+
Domain the project belongs to (name or ID).
53+
This can be used in case collisions between project names exist.
54+
55+
.. option:: --subnet-pool <subnet-pool>
56+
57+
Subnet pool from which this subnet will obtain a CIDR (name or ID)
58+
59+
.. option:: --use-default-subnet-pool
60+
61+
Use default subnet pool for --ip-version
62+
63+
.. option:: --prefix-length <prefix-length>
64+
65+
Prefix length for subnet allocation from subnet pool
66+
67+
.. option:: --subnet-range <subnet-range>
68+
69+
Subnet range in CIDR notation
70+
(required if --subnet-pool is not specified, optional otherwise)
71+
72+
.. option:: --allocation-pool start=<ip-address>,end=<ip-address>
73+
74+
Allocation pool IP addresses for this subnet e.g.:
75+
start=192.168.199.2,end=192.168.199.254 (This option can be repeated)
76+
77+
.. option:: --dhcp
78+
79+
Enable DHCP (default)
80+
81+
.. option:: --no-dhcp
82+
83+
Disable DHCP
84+
85+
.. option:: --dns-nameserver <dns-nameserver>
86+
87+
DNS name server for this subnet (This option can be repeated)
88+
89+
.. option:: --gateway <gateway>
90+
91+
Specify a gateway for the subnet. The three options are:
92+
<ip-address>: Specific IP address to use as the gateway
93+
'auto': Gateway address should automatically be chosen from
94+
within the subnet itself
95+
'none': This subnet will not use a gateway
96+
e.g.: --gateway 192.168.9.1, --gateway auto, --gateway none
97+
(default is 'auto')
98+
99+
.. option:: --host-route destination=<subnet>,gateway=<ip-address>
100+
101+
Additional route for this subnet e.g.:
102+
destination=10.10.0.0/16,gateway=192.168.71.254
103+
destination: destination subnet (in CIDR notation)
104+
gateway: nexthop IP address
105+
(This option can be repeated)
106+
107+
.. option:: --ip-version {4,6}
108+
109+
IP version (default is 4). Note that when subnet pool is specified,
110+
IP version is determined from the subnet pool and this option
111+
is ignored.
112+
113+
.. option:: --ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}
114+
115+
IPv6 RA (Router Advertisement) mode,
116+
valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]
117+
118+
.. option:: --ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}
119+
120+
IPv6 address mode, valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]
121+
122+
.. option:: --network <network>
123+
124+
Network this subnet belongs to (name or ID)
125+
126+
.. _subnet_create-name:
127+
.. describe:: <name>
128+
129+
Name of subnet to create
130+
23131
subnet list
24132
-----------
25133

openstackclient/network/v2/subnet.py

Lines changed: 223 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@
1212
#
1313

1414
"""Subnet action implementations"""
15+
import copy
16+
17+
from json.encoder import JSONEncoder
1518

1619
from openstackclient.common import command
20+
from openstackclient.common import parseractions
1721
from openstackclient.common import utils
22+
from openstackclient.identity import common as identity_common
1823

1924

2025
def _format_allocation_pools(data):
@@ -23,10 +28,17 @@ def _format_allocation_pools(data):
2328
return ','.join(pool_formatted)
2429

2530

31+
def _format_host_routes(data):
32+
try:
33+
return '\n'.join([JSONEncoder().encode(route) for route in data])
34+
except (TypeError, KeyError):
35+
return ''
36+
37+
2638
_formatters = {
2739
'allocation_pools': _format_allocation_pools,
2840
'dns_nameservers': utils.format_list,
29-
'host_routes': utils.format_list,
41+
'host_routes': _format_host_routes,
3042
}
3143

3244

@@ -38,6 +50,214 @@ def _get_columns(item):
3850
return tuple(sorted(columns))
3951

4052

53+
def convert_entries_to_nexthop(entries):
54+
# Change 'gateway' entry to 'nexthop'
55+
changed_entries = copy.deepcopy(entries)
56+
for entry in changed_entries:
57+
entry['nexthop'] = entry['gateway']
58+
del entry['gateway']
59+
60+
return changed_entries
61+
62+
63+
def convert_entries_to_gateway(entries):
64+
# Change 'nexhop' entry to 'gateway'
65+
changed_entries = copy.deepcopy(entries)
66+
for entry in changed_entries:
67+
entry['gateway'] = entry['nexthop']
68+
del entry['nexthop']
69+
70+
return changed_entries
71+
72+
73+
def _get_attrs(client_manager, parsed_args):
74+
attrs = {}
75+
if parsed_args.name is not None:
76+
attrs['name'] = str(parsed_args.name)
77+
78+
if 'project' in parsed_args and parsed_args.project is not None:
79+
identity_client = client_manager.identity
80+
project_id = identity_common.find_project(
81+
identity_client,
82+
parsed_args.project,
83+
parsed_args.project_domain,
84+
).id
85+
attrs['tenant_id'] = project_id
86+
87+
client = client_manager.network
88+
attrs['network_id'] = client.find_network(parsed_args.network,
89+
ignore_missing=False).id
90+
91+
if parsed_args.subnet_pool is not None:
92+
subnet_pool = client.find_subnet_pool(parsed_args.subnet_pool,
93+
ignore_missing=False)
94+
attrs['subnetpool_id'] = subnet_pool.id
95+
96+
if parsed_args.use_default_subnet_pool:
97+
attrs['use_default_subnetpool'] = True
98+
if parsed_args.gateway.lower() != 'auto':
99+
if parsed_args.gateway.lower() == 'none':
100+
attrs['gateway_ip'] = None
101+
else:
102+
attrs['gateway_ip'] = parsed_args.gateway
103+
if parsed_args.prefix_length is not None:
104+
attrs['prefixlen'] = parsed_args.prefix_length
105+
if parsed_args.subnet_range is not None:
106+
attrs['cidr'] = parsed_args.subnet_range
107+
if parsed_args.ip_version is not None:
108+
attrs['ip_version'] = parsed_args.ip_version
109+
if parsed_args.ipv6_ra_mode is not None:
110+
attrs['ipv6_ra_mode'] = parsed_args.ipv6_ra_mode
111+
if parsed_args.ipv6_address_mode is not None:
112+
attrs['ipv6_address_mode'] = parsed_args.ipv6_address_mode
113+
if parsed_args.allocation_pools is not None:
114+
attrs['allocation_pools'] = parsed_args.allocation_pools
115+
if parsed_args.enable_dhcp is not None:
116+
attrs['enable_dhcp'] = parsed_args.enable_dhcp
117+
if parsed_args.dns_nameservers is not None:
118+
attrs['dns_nameservers'] = parsed_args.dns_nameservers
119+
if parsed_args.host_routes is not None:
120+
# Change 'gateway' entry to 'nexthop' to match the API
121+
attrs['host_routes'] = convert_entries_to_nexthop(
122+
parsed_args.host_routes)
123+
124+
return attrs
125+
126+
127+
class CreateSubnet(command.ShowOne):
128+
"""Create a subnet"""
129+
130+
def get_parser(self, prog_name):
131+
parser = super(CreateSubnet, self).get_parser(prog_name)
132+
parser.add_argument(
133+
'name',
134+
help='New subnet name',
135+
)
136+
parser.add_argument(
137+
'--project',
138+
metavar='<project>',
139+
help="Owner's project (name or ID)",
140+
)
141+
identity_common.add_project_domain_option_to_parser(parser)
142+
subnet_pool_group = parser.add_mutually_exclusive_group()
143+
subnet_pool_group.add_argument(
144+
'--subnet-pool',
145+
metavar='<subnet-pool>',
146+
help='Subnet pool from which this subnet will obtain a CIDR '
147+
'(Name or ID)',
148+
)
149+
subnet_pool_group.add_argument(
150+
'--use-default-subnet-pool',
151+
action='store_true',
152+
help='Use default subnet pool for --ip-version',
153+
)
154+
parser.add_argument(
155+
'--prefix-length',
156+
metavar='<prefix-length>',
157+
help='Prefix length for subnet allocation from subnetpool',
158+
)
159+
parser.add_argument(
160+
'--subnet-range',
161+
metavar='<subnet-range>',
162+
help='Subnet range in CIDR notation '
163+
'(required if --subnet-pool is not specified, '
164+
'optional otherwise)',
165+
)
166+
parser.add_argument(
167+
'--allocation-pool',
168+
metavar='start=<ip-address>,end=<ip-address>',
169+
dest='allocation_pools',
170+
action=parseractions.MultiKeyValueAction,
171+
required_keys=['start', 'end'],
172+
help='Allocation pool IP addresses for this subnet '
173+
'e.g.: start=192.168.199.2,end=192.168.199.254 '
174+
'(This option can be repeated)',
175+
)
176+
dhcp_enable_group = parser.add_mutually_exclusive_group()
177+
dhcp_enable_group.add_argument(
178+
'--dhcp',
179+
dest='enable_dhcp',
180+
action='store_true',
181+
default=True,
182+
help='Enable DHCP (default)',
183+
)
184+
dhcp_enable_group.add_argument(
185+
'--no-dhcp',
186+
dest='enable_dhcp',
187+
action='store_false',
188+
help='Disable DHCP',
189+
)
190+
parser.add_argument(
191+
'--dns-nameserver',
192+
metavar='<dns-nameserver>',
193+
action='append',
194+
dest='dns_nameservers',
195+
help='DNS name server for this subnet '
196+
'(This option can be repeated)',
197+
)
198+
parser.add_argument(
199+
'--gateway',
200+
metavar='<gateway>',
201+
default='auto',
202+
help="Specify a gateway for the subnet. The three options are: "
203+
" <ip-address>: Specific IP address to use as the gateway "
204+
" 'auto': Gateway address should automatically be "
205+
" chosen from within the subnet itself "
206+
" 'none': This subnet will not use a gateway "
207+
"e.g.: --gateway 192.168.9.1, --gateway auto, --gateway none"
208+
"(default is 'auto')",
209+
)
210+
parser.add_argument(
211+
'--host-route',
212+
metavar='destination=<subnet>,gateway=<ip-address>',
213+
dest='host_routes',
214+
action=parseractions.MultiKeyValueAction,
215+
required_keys=['destination', 'gateway'],
216+
help='Additional route for this subnet '
217+
'e.g.: destination=10.10.0.0/16,gateway=192.168.71.254 '
218+
'destination: destination subnet (in CIDR notation) '
219+
'gateway: nexthop IP address '
220+
'(This option can be repeated)',
221+
)
222+
parser.add_argument(
223+
'--ip-version',
224+
type=int,
225+
default=4,
226+
choices=[4, 6],
227+
help='IP version (default is 4). Note that when subnet pool is '
228+
'specified, IP version is determined from the subnet pool '
229+
'and this option is ignored.',
230+
)
231+
parser.add_argument(
232+
'--ipv6-ra-mode',
233+
choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'],
234+
help='IPv6 RA (Router Advertisement) mode, '
235+
'valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]',
236+
)
237+
parser.add_argument(
238+
'--ipv6-address-mode',
239+
choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'],
240+
help='IPv6 address mode, '
241+
'valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]',
242+
)
243+
parser.add_argument(
244+
'--network',
245+
required=True,
246+
metavar='<network>',
247+
help='Network this subnet belongs to (name or ID)',
248+
)
249+
250+
return parser
251+
252+
def take_action(self, parsed_args):
253+
client = self.app.client_manager.network
254+
attrs = _get_attrs(self.app.client_manager, parsed_args)
255+
obj = client.create_subnet(**attrs)
256+
columns = _get_columns(obj)
257+
data = utils.get_item_properties(obj, columns, formatters=_formatters)
258+
return (columns, data)
259+
260+
41261
class DeleteSubnet(command.Command):
42262
"""Delete subnet"""
43263

@@ -46,7 +266,7 @@ def get_parser(self, prog_name):
46266
parser.add_argument(
47267
'subnet',
48268
metavar="<subnet>",
49-
help="Subnet to delete (name or ID)"
269+
help="Subnet to delete (name or ID)",
50270
)
51271
return parser
52272

@@ -97,7 +317,7 @@ def get_parser(self, prog_name):
97317
parser.add_argument(
98318
'subnet',
99319
metavar="<subnet>",
100-
help="Subnet to show (name or ID)"
320+
help="Subnet to show (name or ID)",
101321
)
102322
return parser
103323

openstackclient/tests/network/v2/fakes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ def create_one_subnet(attrs={}, methods={}):
573573
'dns_nameservers': [],
574574
'allocation_pools': [],
575575
'host_routes': [],
576-
'ip_version': '4',
576+
'ip_version': 4,
577577
'gateway_ip': '10.10.10.1',
578578
'ipv6_address_mode': 'None',
579579
'ipv6_ra_mode': 'None',

0 commit comments

Comments
 (0)