OpenStack Dashboard (Horizon)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

workflows.py 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. # Copyright 2012 NEC Corporation
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. import logging
  15. from django.conf import settings
  16. from django.core.urlresolvers import reverse
  17. from django.utils.translation import ugettext_lazy as _
  18. import netaddr
  19. from horizon import exceptions
  20. from horizon import forms
  21. from horizon import messages
  22. from horizon import workflows
  23. from openstack_dashboard import api
  24. from openstack_dashboard.dashboards.project.networks.subnets import utils
  25. from openstack_dashboard import policy
  26. LOG = logging.getLogger(__name__)
  27. class CreateNetworkInfoAction(workflows.Action):
  28. net_name = forms.CharField(max_length=255,
  29. label=_("Network Name"),
  30. required=False)
  31. admin_state = forms.BooleanField(
  32. label=_("Enable Admin State"),
  33. initial=True,
  34. required=False,
  35. help_text=_("The state to start the network in."))
  36. shared = forms.BooleanField(label=_("Shared"), initial=False,
  37. required=False)
  38. with_subnet = forms.BooleanField(label=_("Create Subnet"),
  39. widget=forms.CheckboxInput(attrs={
  40. 'class': 'switchable',
  41. 'data-slug': 'with_subnet',
  42. 'data-hide-tab': 'create_network__'
  43. 'createsubnetinfo'
  44. 'action,'
  45. 'create_network__'
  46. 'createsubnetdetail'
  47. 'action',
  48. 'data-hide-on-checked': 'false'
  49. }),
  50. initial=True,
  51. required=False)
  52. def __init__(self, request, *args, **kwargs):
  53. super(CreateNetworkInfoAction, self).__init__(request,
  54. *args, **kwargs)
  55. if not policy.check((("network", "create_network:shared"),), request):
  56. self.fields['shared'].widget = forms.HiddenInput()
  57. class Meta(object):
  58. name = _("Network")
  59. help_text = _('Create a new network. '
  60. 'In addition, a subnet associated with the network '
  61. 'can be created in the following steps of this wizard.')
  62. class CreateNetworkInfo(workflows.Step):
  63. action_class = CreateNetworkInfoAction
  64. contributes = ("net_name", "admin_state", "with_subnet", "shared")
  65. class CreateSubnetInfoAction(workflows.Action):
  66. subnet_name = forms.CharField(max_length=255,
  67. widget=forms.TextInput(attrs={
  68. }),
  69. label=_("Subnet Name"),
  70. required=False)
  71. address_source = forms.ChoiceField(
  72. required=False,
  73. label=_('Network Address Source'),
  74. choices=[('manual', _('Enter Network Address manually')),
  75. ('subnetpool', _('Allocate Network Address from a pool'))],
  76. widget=forms.ThemableSelectWidget(attrs={
  77. 'class': 'switchable',
  78. 'data-slug': 'source',
  79. }))
  80. subnetpool = forms.ChoiceField(
  81. label=_("Address pool"),
  82. widget=forms.ThemableSelectWidget(attrs={
  83. 'class': 'switched switchable',
  84. 'data-slug': 'subnetpool',
  85. 'data-switch-on': 'source',
  86. 'data-source-subnetpool': _('Address pool')},
  87. data_attrs=('name', 'prefixes',
  88. 'ip_version',
  89. 'min_prefixlen',
  90. 'max_prefixlen',
  91. 'default_prefixlen'),
  92. transform=lambda x: "%s (%s)" % (x.name, ", ".join(x.prefixes))
  93. if 'prefixes' in x else "%s" % (x.name)),
  94. required=False)
  95. prefixlen = forms.ChoiceField(widget=forms.ThemableSelectWidget(attrs={
  96. 'class': 'switched',
  97. 'data-switch-on': 'subnetpool',
  98. }),
  99. label=_('Network Mask'),
  100. required=False)
  101. cidr = forms.IPField(label=_("Network Address"),
  102. required=False,
  103. initial="",
  104. widget=forms.TextInput(attrs={
  105. 'class': 'switched',
  106. 'data-switch-on': 'source',
  107. 'data-source-manual': _("Network Address"),
  108. }),
  109. help_text=_("Network address in CIDR format "
  110. "(e.g. 192.168.0.0/24, 2001:DB8::/48)"),
  111. version=forms.IPv4 | forms.IPv6,
  112. mask=True)
  113. ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')],
  114. widget=forms.ThemableSelectWidget(attrs={
  115. 'class': 'switchable',
  116. 'data-slug': 'ipversion',
  117. }),
  118. label=_("IP Version"),
  119. required=False)
  120. gateway_ip = forms.IPField(
  121. label=_("Gateway IP"),
  122. widget=forms.TextInput(attrs={
  123. 'class': 'switched',
  124. 'data-switch-on': 'gateway_ip',
  125. 'data-source-manual': _("Gateway IP")
  126. }),
  127. required=False,
  128. initial="",
  129. help_text=_("IP address of Gateway (e.g. 192.168.0.254) "
  130. "The default value is the first IP of the "
  131. "network address "
  132. "(e.g. 192.168.0.1 for 192.168.0.0/24, "
  133. "2001:DB8::1 for 2001:DB8::/48). "
  134. "If you use the default, leave blank. "
  135. "If you do not want to use a gateway, "
  136. "check 'Disable Gateway' below."),
  137. version=forms.IPv4 | forms.IPv6,
  138. mask=False)
  139. no_gateway = forms.BooleanField(label=_("Disable Gateway"),
  140. widget=forms.CheckboxInput(attrs={
  141. 'class': 'switchable',
  142. 'data-slug': 'gateway_ip',
  143. 'data-hide-on-checked': 'true'
  144. }),
  145. initial=False,
  146. required=False)
  147. check_subnet_range = True
  148. class Meta(object):
  149. name = _("Subnet")
  150. help_text = _('Creates a subnet associated with the network.'
  151. ' You need to enter a valid "Network Address"'
  152. ' and "Gateway IP". If you did not enter the'
  153. ' "Gateway IP", the first value of a network'
  154. ' will be assigned by default. If you do not want'
  155. ' gateway please check the "Disable Gateway" checkbox.'
  156. ' Advanced configuration is available by clicking on'
  157. ' the "Subnet Details" tab.')
  158. def __init__(self, request, context, *args, **kwargs):
  159. super(CreateSubnetInfoAction, self).__init__(request, context, *args,
  160. **kwargs)
  161. if 'with_subnet' in context:
  162. self.fields['with_subnet'] = forms.BooleanField(
  163. initial=context['with_subnet'],
  164. required=False,
  165. widget=forms.HiddenInput()
  166. )
  167. if not getattr(settings, 'OPENSTACK_NEUTRON_NETWORK',
  168. {}).get('enable_ipv6', True):
  169. self.fields['ip_version'].widget = forms.HiddenInput()
  170. self.fields['ip_version'].initial = 4
  171. try:
  172. if api.neutron.is_extension_supported(request,
  173. 'subnet_allocation'):
  174. self.fields['subnetpool'].choices = \
  175. self.get_subnetpool_choices(request)
  176. else:
  177. self.hide_subnetpool_choices()
  178. except Exception:
  179. self.hide_subnetpool_choices()
  180. msg = _('Unable to initialize subnetpools')
  181. exceptions.handle(request, msg)
  182. if len(self.fields['subnetpool'].choices) > 1:
  183. # Pre-populate prefixlen choices to satisfy Django
  184. # ChoiceField Validation. This is overridden w/data from
  185. # subnetpool on select.
  186. self.fields['prefixlen'].choices = \
  187. zip(list(range(0, 128 + 1)),
  188. list(range(0, 128 + 1)))
  189. # Populate data-fields for switching the prefixlen field
  190. # when user selects a subnetpool other than
  191. # "Provider default pool"
  192. for (id, name) in self.fields['subnetpool'].choices:
  193. if not len(id):
  194. continue
  195. key = 'data-subnetpool-' + id
  196. self.fields['prefixlen'].widget.attrs[key] = \
  197. _('Network Mask')
  198. else:
  199. self.hide_subnetpool_choices()
  200. def get_subnetpool_choices(self, request):
  201. subnetpool_choices = [('', _('Select a pool'))]
  202. for subnetpool in api.neutron.subnetpool_list(request):
  203. subnetpool_choices.append((subnetpool.id, subnetpool))
  204. return subnetpool_choices
  205. def hide_subnetpool_choices(self):
  206. self.fields['address_source'].widget = forms.HiddenInput()
  207. self.fields['subnetpool'].choices = []
  208. self.fields['subnetpool'].widget = forms.HiddenInput()
  209. self.fields['prefixlen'].widget = forms.HiddenInput()
  210. def _check_subnet_range(self, subnet, allow_cidr):
  211. allowed_net = netaddr.IPNetwork(allow_cidr)
  212. return subnet in allowed_net
  213. def _check_cidr_allowed(self, ip_version, subnet):
  214. if not self.check_subnet_range:
  215. return
  216. allowed_cidr = getattr(settings, "ALLOWED_PRIVATE_SUBNET_CIDR", {})
  217. version_str = 'ipv%s' % ip_version
  218. allowed_ranges = allowed_cidr.get(version_str, [])
  219. if allowed_ranges:
  220. under_range = any(self._check_subnet_range(subnet, allowed_range)
  221. for allowed_range in allowed_ranges)
  222. if not under_range:
  223. range_str = ', '.join(allowed_ranges)
  224. msg = (_("CIDRs allowed for user private %(ip_ver)s "
  225. "networks are %(allowed)s.") %
  226. {'ip_ver': '%s' % version_str,
  227. 'allowed': range_str})
  228. raise forms.ValidationError(msg)
  229. def _check_subnet_data(self, cleaned_data, is_create=True):
  230. cidr = cleaned_data.get('cidr')
  231. ip_version = int(cleaned_data.get('ip_version'))
  232. gateway_ip = cleaned_data.get('gateway_ip')
  233. no_gateway = cleaned_data.get('no_gateway')
  234. address_source = cleaned_data.get('address_source')
  235. subnetpool = cleaned_data.get('subnetpool')
  236. if not subnetpool and address_source == 'subnetpool':
  237. msg = _('Specify "Address pool" or select '
  238. '"Enter Network Address manually" and specify '
  239. '"Network Address".')
  240. raise forms.ValidationError(msg)
  241. if not cidr and address_source != 'subnetpool':
  242. msg = _('Specify "Network Address" or '
  243. 'clear "Create Subnet" checkbox in previous step.')
  244. raise forms.ValidationError(msg)
  245. if cidr:
  246. subnet = netaddr.IPNetwork(cidr)
  247. if subnet.version != ip_version:
  248. msg = _('Network Address and IP version are inconsistent.')
  249. raise forms.ValidationError(msg)
  250. if (ip_version == 4 and subnet.prefixlen == 32) or \
  251. (ip_version == 6 and subnet.prefixlen == 128):
  252. msg = _("The subnet in the Network Address is "
  253. "too small (/%s).") % subnet.prefixlen
  254. self._errors['cidr'] = self.error_class([msg])
  255. self._check_cidr_allowed(ip_version, subnet)
  256. if not no_gateway and gateway_ip:
  257. if netaddr.IPAddress(gateway_ip).version is not ip_version:
  258. msg = _('Gateway IP and IP version are inconsistent.')
  259. raise forms.ValidationError(msg)
  260. if not is_create and not no_gateway and not gateway_ip:
  261. msg = _('Specify IP address of gateway or '
  262. 'check "Disable Gateway" checkbox.')
  263. raise forms.ValidationError(msg)
  264. def clean(self):
  265. cleaned_data = super(CreateSubnetInfoAction, self).clean()
  266. with_subnet = cleaned_data.get('with_subnet')
  267. if not with_subnet:
  268. return cleaned_data
  269. self._check_subnet_data(cleaned_data)
  270. return cleaned_data
  271. class CreateSubnetInfo(workflows.Step):
  272. action_class = CreateSubnetInfoAction
  273. contributes = ("subnet_name", "cidr", "ip_version",
  274. "gateway_ip", "no_gateway", "subnetpool",
  275. "prefixlen", "address_source")
  276. class CreateSubnetDetailAction(workflows.Action):
  277. enable_dhcp = forms.BooleanField(label=_("Enable DHCP"),
  278. initial=True, required=False)
  279. ipv6_modes = forms.ChoiceField(
  280. label=_("IPv6 Address Configuration Mode"),
  281. widget=forms.ThemableSelectWidget(attrs={
  282. 'class': 'switched',
  283. 'data-switch-on': 'ipversion',
  284. 'data-ipversion-6': _("IPv6 Address Configuration Mode"),
  285. }),
  286. initial=utils.IPV6_DEFAULT_MODE,
  287. required=False,
  288. help_text=_("Specifies how IPv6 addresses and additional information "
  289. "are configured. We can specify SLAAC/DHCPv6 stateful/"
  290. "DHCPv6 stateless provided by OpenStack, "
  291. "or specify no option. "
  292. "'No options specified' means addresses are configured "
  293. "manually or configured by a non-OpenStack system."))
  294. allocation_pools = forms.CharField(
  295. widget=forms.Textarea(attrs={'rows': 4}),
  296. label=_("Allocation Pools"),
  297. help_text=_("IP address allocation pools. Each entry is: "
  298. "start_ip_address,end_ip_address "
  299. "(e.g., 192.168.1.100,192.168.1.120) "
  300. "and one entry per line."),
  301. required=False)
  302. dns_nameservers = forms.CharField(
  303. widget=forms.widgets.Textarea(attrs={'rows': 4}),
  304. label=_("DNS Name Servers"),
  305. help_text=_("IP address list of DNS name servers for this subnet. "
  306. "One entry per line."),
  307. required=False)
  308. host_routes = forms.CharField(
  309. widget=forms.widgets.Textarea(attrs={'rows': 4}),
  310. label=_("Host Routes"),
  311. help_text=_("Additional routes announced to the hosts. "
  312. "Each entry is: destination_cidr,nexthop "
  313. "(e.g., 192.168.200.0/24,10.56.1.254) "
  314. "and one entry per line."),
  315. required=False)
  316. class Meta(object):
  317. name = _("Subnet Details")
  318. help_text = _('Specify additional attributes for the subnet.')
  319. def __init__(self, request, context, *args, **kwargs):
  320. super(CreateSubnetDetailAction, self).__init__(request, context,
  321. *args, **kwargs)
  322. if not getattr(settings, 'OPENSTACK_NEUTRON_NETWORK',
  323. {}).get('enable_ipv6', True):
  324. self.fields['ipv6_modes'].widget = forms.HiddenInput()
  325. def populate_ipv6_modes_choices(self, request, context):
  326. return [(value, _("%s (Default)") % label)
  327. if value == utils.IPV6_DEFAULT_MODE
  328. else (value, label)
  329. for value, label in utils.IPV6_MODE_CHOICES]
  330. def _convert_ip_address(self, ip, field_name):
  331. try:
  332. return netaddr.IPAddress(ip)
  333. except (netaddr.AddrFormatError, ValueError):
  334. msg = (_('%(field_name)s: Invalid IP address (value=%(ip)s)')
  335. % {'field_name': field_name, 'ip': ip})
  336. raise forms.ValidationError(msg)
  337. def _convert_ip_network(self, network, field_name):
  338. try:
  339. return netaddr.IPNetwork(network)
  340. except (netaddr.AddrFormatError, ValueError):
  341. msg = (_('%(field_name)s: Invalid IP address (value=%(network)s)')
  342. % {'field_name': field_name, 'network': network})
  343. raise forms.ValidationError(msg)
  344. def _check_allocation_pools(self, allocation_pools):
  345. for p in allocation_pools.splitlines():
  346. p = p.strip()
  347. if not p:
  348. continue
  349. pool = p.split(',')
  350. if len(pool) != 2:
  351. msg = _('Start and end addresses must be specified '
  352. '(value=%s)') % p
  353. raise forms.ValidationError(msg)
  354. start, end = [self._convert_ip_address(ip, "allocation_pools")
  355. for ip in pool]
  356. if start > end:
  357. msg = _('Start address is larger than end address '
  358. '(value=%s)') % p
  359. raise forms.ValidationError(msg)
  360. def _check_dns_nameservers(self, dns_nameservers):
  361. for ns in dns_nameservers.splitlines():
  362. ns = ns.strip()
  363. if not ns:
  364. continue
  365. self._convert_ip_address(ns, "dns_nameservers")
  366. def _check_host_routes(self, host_routes):
  367. for r in host_routes.splitlines():
  368. r = r.strip()
  369. if not r:
  370. continue
  371. route = r.split(',')
  372. if len(route) != 2:
  373. msg = _('Host Routes format error: '
  374. 'Destination CIDR and nexthop must be specified '
  375. '(value=%s)') % r
  376. raise forms.ValidationError(msg)
  377. self._convert_ip_network(route[0], "host_routes")
  378. self._convert_ip_address(route[1], "host_routes")
  379. def clean(self):
  380. cleaned_data = super(CreateSubnetDetailAction, self).clean()
  381. self._check_allocation_pools(cleaned_data.get('allocation_pools'))
  382. self._check_host_routes(cleaned_data.get('host_routes'))
  383. self._check_dns_nameservers(cleaned_data.get('dns_nameservers'))
  384. return cleaned_data
  385. class CreateSubnetDetail(workflows.Step):
  386. action_class = CreateSubnetDetailAction
  387. contributes = ("enable_dhcp", "ipv6_modes", "allocation_pools",
  388. "dns_nameservers", "host_routes")
  389. class CreateNetwork(workflows.Workflow):
  390. slug = "create_network"
  391. name = _("Create Network")
  392. finalize_button_name = _("Create")
  393. success_message = _('Created network "%s".')
  394. failure_message = _('Unable to create network "%s".')
  395. default_steps = (CreateNetworkInfo,
  396. CreateSubnetInfo,
  397. CreateSubnetDetail)
  398. wizard = True
  399. def get_success_url(self):
  400. return reverse("horizon:project:networks:index")
  401. def get_failure_url(self):
  402. return reverse("horizon:project:networks:index")
  403. def format_status_message(self, message):
  404. name = self.context.get('net_name') or self.context.get('net_id', '')
  405. return message % name
  406. def _create_network(self, request, data):
  407. try:
  408. params = {'name': data['net_name'],
  409. 'admin_state_up': data['admin_state'],
  410. 'shared': data['shared']}
  411. network = api.neutron.network_create(request, **params)
  412. self.context['net_id'] = network.id
  413. LOG.debug('Network "%s" was successfully created.',
  414. network.name_or_id)
  415. return network
  416. except Exception as e:
  417. LOG.info('Failed to create network: %s', e)
  418. msg = (_('Failed to create network "%(network)s": %(reason)s') %
  419. {"network": data['net_name'], "reason": e})
  420. redirect = self.get_failure_url()
  421. exceptions.handle(request, msg, redirect=redirect)
  422. return False
  423. def _setup_subnet_parameters(self, params, data, is_create=True):
  424. """Setup subnet parameters
  425. This methods setups subnet parameters which are available
  426. in both create and update.
  427. """
  428. is_update = not is_create
  429. params['enable_dhcp'] = data['enable_dhcp']
  430. if int(data['ip_version']) == 6:
  431. ipv6_modes = utils.get_ipv6_modes_attrs_from_menu(
  432. data['ipv6_modes'])
  433. if ipv6_modes[0] and is_create:
  434. params['ipv6_ra_mode'] = ipv6_modes[0]
  435. if ipv6_modes[1] and is_create:
  436. params['ipv6_address_mode'] = ipv6_modes[1]
  437. if data['allocation_pools']:
  438. pools = [dict(zip(['start', 'end'], pool.strip().split(',')))
  439. for pool in data['allocation_pools'].splitlines()
  440. if pool.strip()]
  441. params['allocation_pools'] = pools
  442. if data['host_routes'] or is_update:
  443. routes = [dict(zip(['destination', 'nexthop'],
  444. route.strip().split(',')))
  445. for route in data['host_routes'].splitlines()
  446. if route.strip()]
  447. params['host_routes'] = routes
  448. if data['dns_nameservers'] or is_update:
  449. nameservers = [ns.strip()
  450. for ns in data['dns_nameservers'].splitlines()
  451. if ns.strip()]
  452. params['dns_nameservers'] = nameservers
  453. def _create_subnet(self, request, data, network=None, tenant_id=None,
  454. no_redirect=False):
  455. if network:
  456. network_id = network.id
  457. network_name = network.name
  458. else:
  459. network_id = self.context.get('network_id')
  460. network_name = self.context.get('network_name')
  461. try:
  462. params = {'network_id': network_id,
  463. 'name': data['subnet_name']}
  464. if 'cidr' in data and data['cidr']:
  465. params['cidr'] = data['cidr']
  466. if 'ip_version' in data and data['ip_version']:
  467. params['ip_version'] = int(data['ip_version'])
  468. if tenant_id:
  469. params['tenant_id'] = tenant_id
  470. if data['no_gateway']:
  471. params['gateway_ip'] = None
  472. elif data['gateway_ip']:
  473. params['gateway_ip'] = data['gateway_ip']
  474. if 'subnetpool' in data and len(data['subnetpool']):
  475. params['subnetpool_id'] = data['subnetpool']
  476. if 'prefixlen' in data and len(data['prefixlen']):
  477. params['prefixlen'] = data['prefixlen']
  478. self._setup_subnet_parameters(params, data)
  479. subnet = api.neutron.subnet_create(request, **params)
  480. self.context['subnet_id'] = subnet.id
  481. LOG.debug('Subnet "%s" was successfully created.', data['cidr'])
  482. return subnet
  483. except Exception as e:
  484. if network_name:
  485. msg = _('Failed to create subnet "%(sub)s" for network '
  486. '"%(net)s": %(reason)s')
  487. else:
  488. msg = _('Failed to create subnet "%(sub)s": %(reason)s')
  489. if no_redirect:
  490. redirect = None
  491. else:
  492. redirect = self.get_failure_url()
  493. exceptions.handle(request,
  494. msg % {"sub": data['cidr'], "net": network_name,
  495. "reason": e},
  496. redirect=redirect)
  497. return False
  498. def _delete_network(self, request, network):
  499. """Delete the created network when subnet creation failed."""
  500. try:
  501. api.neutron.network_delete(request, network.id)
  502. LOG.debug('Delete the created network %s '
  503. 'due to subnet creation failure.', network.id)
  504. msg = _('Delete the created network "%s" '
  505. 'due to subnet creation failure.') % network.name
  506. redirect = self.get_failure_url()
  507. messages.info(request, msg)
  508. raise exceptions.Http302(redirect)
  509. except Exception as e:
  510. LOG.info('Failed to delete network %(id)s: %(exc)s',
  511. {'id': network.id, 'exc': e})
  512. msg = _('Failed to delete network "%s"') % network.name
  513. redirect = self.get_failure_url()
  514. exceptions.handle(request, msg, redirect=redirect)
  515. def handle(self, request, data):
  516. network = self._create_network(request, data)
  517. if not network:
  518. return False
  519. # If we do not need to create a subnet, return here.
  520. if not data['with_subnet']:
  521. return True
  522. subnet = self._create_subnet(request, data, network, no_redirect=True)
  523. if subnet:
  524. return True
  525. else:
  526. self._delete_network(request, network)
  527. return False