Implement OS::Neutron::ExtraRoute as /contrib

This codes aims to add support the ExtraRoute resources.

  * Neutron Router ExtraRoute as OS::Neutron::ExtraRoute
  * OS::Neutron::ExtraRoute is implemented in /contrib directory as plugin
  * The resource can be enabled by setting plugin_dirs property in heat.conf

Co-Authored-By: Kevin Benton <kevin.benton@bigswitch.com>
(from https://review.openstack.org/#/c/41044/)

Change-Id: I27dabe42e6c96a475aa2f31b091616c89eef83a7
Implements: blueprint extraroute-as-contrib
This commit is contained in:
Mitsuru Kanabuchi 2014-03-04 14:19:17 +09:00
parent 5160a76b9c
commit 897c7564d0
6 changed files with 295 additions and 0 deletions

View File

@ -0,0 +1,38 @@
ExtraRoute plugin for OpenStack Heat
====================================
This plugin enables using ExtraRoute as a resource in a Heat template.
This resource allows assigning extra routes to Neutron routers via Heat
templates.
NOTE: Implementing ExtraRoute in the main heat tree is under discussion in the
heat community.
This plugin has been implemented in contrib to provide access to the
functionality while the discussion takes place, as some users have an immediate
requirement for it.
It may be moved to the main heat tree in due-course, depending on the outcome
of the community discussion.
### 1. Install the ExtraRoute plugin in Heat
NOTE: Heat scans several directories to find plugins. The list of directories
is specified in the configuration file "heat.conf" with the "plugin_dirs"
directive.
### 2. Restart heat
Only the process "heat-engine" needs to be restarted to load the newly
installed plugin.
### 3. Example of ExtraRoute
"router_extraroute": {
"Type": "OS::Neutron::ExtraRoute",
"Properties": {
"router_id": { "Ref" : "router" },
"destination": "172.16.0.0/24",
"nexthop": "192.168.0.254"
}
}

View File

@ -0,0 +1,105 @@
#
# 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.
from heat.common import exception
from heat.engine import clients
from heat.engine.resources.neutron import neutron
from heat.engine import properties
if clients.neutronclient is not None:
from neutronclient.common.exceptions import NeutronClientException
class ExtraRoute(neutron.NeutronResource):
PROPERTIES = (
ROUTER_ID, DESTINATION, NEXTHOP,
) = (
'router_id', 'destination', 'nexthop',
)
properties_schema = {
ROUTER_ID: properties.Schema(
properties.Schema.STRING,
description=_('The router id.'),
required=True),
DESTINATION: properties.Schema(
properties.Schema.STRING,
description=_('Network in CIDR notation.'),
required=True),
NEXTHOP: properties.Schema(
properties.Schema.STRING,
description=_('Nexthop IP adddress.'),
required=True)
}
def add_dependencies(self, deps):
super(ExtraRoute, self).add_dependencies(deps)
for resource in self.stack.itervalues():
# depend on any RouterInterface in this template with the same
# router_id as this router_id
if (resource.has_interface('OS::Neutron::RouterInterface') and
resource.properties['router_id'] ==
self.properties['router_id']):
deps += (self, resource)
# depend on any RouterGateway in this template with the same
# router_id as this router_id
elif (resource.has_interface('OS::Neutron::RouterGateway') and
resource.properties['router_id'] ==
self.properties['router_id']):
deps += (self, resource)
def handle_create(self):
router_id = self.properties.get(self.ROUTER_ID)
routes = self.neutron().show_router(
router_id).get('router').get('routes')
if not routes:
routes = []
new_route = {'destination': self.properties[self.DESTINATION],
'nexthop': self.properties[self.NEXTHOP]}
if new_route in routes:
msg = _('Route duplicates an existing route.')
raise exception.Error(msg)
routes.append(new_route)
self.neutron().update_router(router_id, {'router':
{'routes': routes}})
new_route['router_id'] = router_id
self.resource_id_set(
'%(router_id)s:%(destination)s:%(nexthop)s' % new_route)
def handle_delete(self):
if not self.resource_id:
return
(router_id, destination, nexthop) = self.resource_id.split(':')
try:
routes = self.neutron().show_router(
router_id).get('router').get('routes', [])
try:
routes.remove({'destination': destination,
'nexthop': nexthop})
except ValueError:
return
self.neutron().update_router(router_id, {'router':
{'routes': routes}})
except NeutronClientException as ex:
self._handle_not_found_exception(ex)
def resource_mapping():
if clients.neutronclient is None:
return {}
return {
'OS::Neutron::ExtraRoute': ExtraRoute,
}

View File

@ -0,0 +1,152 @@
# 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.
from testtools import skipIf
from heat.engine import clients
from heat.common import template_format
from heat.engine import resource
from heat.engine import scheduler
from heat.engine.resources.neutron import router
from heat.openstack.common.importutils import try_import
from heat.tests.common import HeatTestCase
from heat.tests import fakes
from heat.tests import utils
from ..resources import extraroute # noqa
neutronclient = try_import('neutronclient.v2_0.client')
qe = try_import('neutronclient.common.exceptions')
neutron_template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Template to test OS::Neutron::ExtraRoute resources",
"Parameters" : {},
"Resources" : {
"router": {
"Type": "OS::Neutron::Router"
},
"extraroute1": {
"Type": "OS::Neutron::ExtraRoute",
"Properties": {
"router_id": { "Ref" : "router" },
"destination" : "192.168.0.0/24",
"nexthop": "1.1.1.1"
}
},
"extraroute2": {
"Type": "OS::Neutron::ExtraRoute",
"Properties": {
"router_id": { "Ref" : "router" },
"destination" : "192.168.255.0/24",
"nexthop": "1.1.1.1"
}
}
}
}
'''
@skipIf(neutronclient is None, 'neutronclient unavailable')
class NeutronExtraRouteTest(HeatTestCase):
@skipIf(router.neutronV20 is None, "Missing Neutron v2_0")
def setUp(self):
super(NeutronExtraRouteTest, self).setUp()
self.m.StubOutWithMock(neutronclient.Client, 'show_router')
self.m.StubOutWithMock(neutronclient.Client, 'update_router')
self.m.StubOutWithMock(clients.OpenStackClients, 'keystone')
utils.setup_dummy_db()
resource._register_class("OS::Neutron::ExtraRoute",
extraroute.ExtraRoute)
def create_extraroute(self, t, stack, resource_name, properties={}):
t['Resources'][resource_name]['Properties'] = properties
rsrc = extraroute.ExtraRoute(
resource_name,
t['Resources'][resource_name],
stack)
scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
return rsrc
def test_extraroute(self):
clients.OpenStackClients.keystone().AndReturn(
fakes.FakeKeystoneClient())
# add first route
neutronclient.Client.show_router(
'3e46229d-8fce-4733-819a-b5fe630550f8')\
.AndReturn({'router': {'routes': []}})
neutronclient.Client.update_router(
'3e46229d-8fce-4733-819a-b5fe630550f8',
{"router": {
"routes": [
{"destination": "192.168.0.0/24", "nexthop": "1.1.1.1"},
]
}}).AndReturn(None)
# add second route
neutronclient.Client.show_router(
'3e46229d-8fce-4733-819a-b5fe630550f8')\
.AndReturn({'router': {'routes': [{"destination": "192.168.0.0/24",
"nexthop": "1.1.1.1"}]}})
neutronclient.Client.update_router(
'3e46229d-8fce-4733-819a-b5fe630550f8',
{"router": {
"routes": [
{"destination": "192.168.0.0/24", "nexthop": "1.1.1.1"},
{"destination": "192.168.255.0/24", "nexthop": "1.1.1.1"}
]
}}).AndReturn(None)
# first delete
neutronclient.Client.show_router(
'3e46229d-8fce-4733-819a-b5fe630550f8')\
.AndReturn({'router':
{'routes': [{"destination": "192.168.0.0/24",
"nexthop": "1.1.1.1"},
{"destination": "192.168.255.0/24",
"nexthop": "1.1.1.1"}]}})
neutronclient.Client.update_router(
'3e46229d-8fce-4733-819a-b5fe630550f8',
{"router": {
"routes": [
{"destination": "192.168.255.0/24", "nexthop": "1.1.1.1"}
]
}}).AndReturn(None)
# second delete
neutronclient.Client.show_router(
'3e46229d-8fce-4733-819a-b5fe630550f8')\
.AndReturn({'router':
{'routes': [{"destination": "192.168.255.0/24",
"nexthop": "1.1.1.1"}]}})
self.m.ReplayAll()
t = template_format.parse(neutron_template)
stack = utils.parse_stack(t)
rsrc1 = self.create_extraroute(
t, stack, 'extraroute1', properties={
'router_id': '3e46229d-8fce-4733-819a-b5fe630550f8',
'destination': '192.168.0.0/24',
'nexthop': '1.1.1.1'})
self.create_extraroute(
t, stack, 'extraroute2', properties={
'router_id': '3e46229d-8fce-4733-819a-b5fe630550f8',
'destination': '192.168.255.0/24',
'nexthop': '1.1.1.1'})
scheduler.TaskRunner(rsrc1.delete)()
rsrc1.state_set(rsrc1.CREATE, rsrc1.COMPLETE, 'to delete again')
scheduler.TaskRunner(rsrc1.delete)()
self.m.VerifyAll()