Add a set of native quantum resource types.
The properties schemas map directly to the Quantum REST API, which makes the implementation (and documentation) simpler. The base class QuantumResource contains some default methods and common utility functions. templates/Quantum.template can be run without any parameters and only creates network resources, no instances. More example templates and tests will come later. Change-Id: Ia270294440eeec5163e35009f6be0b5db9ad78c1
This commit is contained in:
parent
b3a57062ad
commit
2162b8047b
0
heat/engine/resources/quantum/__init__.py
Normal file
0
heat/engine/resources/quantum/__init__.py
Normal file
77
heat/engine/resources/quantum/floatingip.py
Normal file
77
heat/engine/resources/quantum/floatingip.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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.openstack.common import log as logging
|
||||||
|
from heat.engine.resources.quantum import quantum
|
||||||
|
|
||||||
|
logger = logging.getLogger('heat.engine.quantum')
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIP(quantum.QuantumResource):
|
||||||
|
properties_schema = {'floating_network_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'value_specs': {'Type': 'Map',
|
||||||
|
'Default': {}},
|
||||||
|
'port_id': {'Type': 'String'},
|
||||||
|
'fixed_ip_address': {'Type': 'String'},
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
props = self.prepare_properties(self.properties, self.name)
|
||||||
|
fip = self.quantum().create_floatingip({
|
||||||
|
'floatingip': props})['floatingip']
|
||||||
|
self.instance_id_set(fip['id'])
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
client.delete_floatingip(self.instance_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def FnGetAtt(self, key):
|
||||||
|
attributes = self.quantum().show_floatingip(
|
||||||
|
self.instance_id)['floatingip']
|
||||||
|
return self.handle_get_attributes(self.name, key, attributes)
|
||||||
|
|
||||||
|
|
||||||
|
class FloatingIPAssociation(quantum.QuantumResource):
|
||||||
|
properties_schema = {'floatingip_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'port_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'fixed_ip_address': {'Type': 'String'}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(FloatingIPAssociation, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
props = self.prepare_properties(self.properties, self.name)
|
||||||
|
|
||||||
|
floatingip_id = props.pop('floatingip_id')
|
||||||
|
|
||||||
|
self.quantum().update_floatingip(floatingip_id, {
|
||||||
|
'floatingip': props})['floatingip']
|
||||||
|
self.instance_id_set('%s:%s' % (floatingip_id, props['port_id']))
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
(floatingip_id, port_id) = self.instance_id.split(':')
|
||||||
|
client.update_floatingip(floatingip_id,
|
||||||
|
{'floatingip': {'port_id': None}})
|
||||||
|
except:
|
||||||
|
pass
|
47
heat/engine/resources/quantum/net.py
Normal file
47
heat/engine/resources/quantum/net.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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.openstack.common import log as logging
|
||||||
|
from heat.engine.resources.quantum import quantum
|
||||||
|
|
||||||
|
logger = logging.getLogger('heat.engine.quantum')
|
||||||
|
|
||||||
|
|
||||||
|
class Net(quantum.QuantumResource):
|
||||||
|
properties_schema = {'name': {'Type': 'String'},
|
||||||
|
'value_specs': {'Type': 'Map',
|
||||||
|
'Default': {}},
|
||||||
|
'admin_state_up': {'Default': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(Net, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
props = self.prepare_properties(self.properties, self.name)
|
||||||
|
net = self.quantum().create_network({'network': props})['network']
|
||||||
|
self.instance_id_set(net['id'])
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
client.delete_network(self.instance_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def FnGetAtt(self, key):
|
||||||
|
attributes = self.quantum().show_network(
|
||||||
|
self.instance_id)['network']
|
||||||
|
return self.handle_get_attributes(self.name, key, attributes)
|
59
heat/engine/resources/quantum/port.py
Normal file
59
heat/engine/resources/quantum/port.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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.openstack.common import log as logging
|
||||||
|
from heat.engine.resources.quantum import quantum
|
||||||
|
|
||||||
|
logger = logging.getLogger('heat.engine.quantum')
|
||||||
|
|
||||||
|
|
||||||
|
class Port(quantum.QuantumResource):
|
||||||
|
|
||||||
|
fixed_ip_schema = {'subnet_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'ip_address': {'Type': 'String',
|
||||||
|
'Required': True}}
|
||||||
|
|
||||||
|
properties_schema = {'network_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'name': {'Type': 'String'},
|
||||||
|
'value_specs': {'Type': 'Map',
|
||||||
|
'Default': {}},
|
||||||
|
'admin_state_up': {'Default': True},
|
||||||
|
'fixed_ips': {'Type': 'List',
|
||||||
|
'Schema': fixed_ip_schema},
|
||||||
|
'mac_address': {'Type': 'String'},
|
||||||
|
'device_id': {'Type': 'String'},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(Port, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
props = self.prepare_properties(self.properties, self.name)
|
||||||
|
port = self.quantum().create_port({'port': props})['port']
|
||||||
|
self.instance_id_set(port['id'])
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
client.delete_port(self.instance_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def FnGetAtt(self, key):
|
||||||
|
attributes = self.quantum().show_port(
|
||||||
|
self.instance_id)['port']
|
||||||
|
return self.handle_get_attributes(self.name, key, attributes)
|
92
heat/engine/resources/quantum/quantum.py
Normal file
92
heat/engine/resources/quantum/quantum.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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.resources import resource
|
||||||
|
|
||||||
|
from heat.openstack.common import log as logging
|
||||||
|
|
||||||
|
logger = logging.getLogger('heat.engine.quantum')
|
||||||
|
|
||||||
|
|
||||||
|
class QuantumResource(resource.Resource):
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(QuantumResource, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
'''
|
||||||
|
Validate any of the provided params
|
||||||
|
'''
|
||||||
|
res = super(QuantumResource, self).validate()
|
||||||
|
if res:
|
||||||
|
return res
|
||||||
|
return self.validate_properties(self.properties)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_properties(properties):
|
||||||
|
'''
|
||||||
|
Validates to ensure nothing in value_specs overwrites
|
||||||
|
any key that exists in the schema.
|
||||||
|
|
||||||
|
Also ensures that shared and tenant_id is not specified
|
||||||
|
in value_specs.
|
||||||
|
'''
|
||||||
|
if 'value_specs' in properties.keys():
|
||||||
|
vs = properties.get('value_specs')
|
||||||
|
banned_keys = set(['shared', 'tenant_id']).union(
|
||||||
|
properties.keys())
|
||||||
|
for k in banned_keys.intersection(vs.keys()):
|
||||||
|
return '%s not allowed in value_specs' % k
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prepare_properties(properties, name):
|
||||||
|
'''
|
||||||
|
Prepares the property values so that they can be passed directly to
|
||||||
|
the Quantum call.
|
||||||
|
|
||||||
|
Removes None values and value_specs, merges value_specs with the main
|
||||||
|
values.
|
||||||
|
'''
|
||||||
|
props = dict((k, v) for k, v in properties.items()
|
||||||
|
if v is not None and k != 'value_specs')
|
||||||
|
|
||||||
|
if 'name' in properties.keys():
|
||||||
|
props.setdefault('name', name)
|
||||||
|
|
||||||
|
if 'value_specs' in properties.keys():
|
||||||
|
props.update(properties.get('value_specs'))
|
||||||
|
|
||||||
|
return props
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handle_get_attributes(name, key, attributes):
|
||||||
|
'''
|
||||||
|
Support method for responding to FnGetAtt
|
||||||
|
'''
|
||||||
|
if key == 'show':
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
if key in attributes.keys():
|
||||||
|
return attributes[key]
|
||||||
|
|
||||||
|
raise exception.InvalidTemplateAttribute(resource=name,
|
||||||
|
key=key)
|
||||||
|
|
||||||
|
def handle_update(self):
|
||||||
|
return self.UPDATE_REPLACE
|
||||||
|
|
||||||
|
def FnGetRefId(self):
|
||||||
|
return unicode(self.instance_id)
|
102
heat/engine/resources/quantum/router.py
Normal file
102
heat/engine/resources/quantum/router.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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.engine.resources.quantum import quantum
|
||||||
|
|
||||||
|
from heat.openstack.common import log as logging
|
||||||
|
|
||||||
|
logger = logging.getLogger('heat.engine.quantum')
|
||||||
|
|
||||||
|
|
||||||
|
class Router(quantum.QuantumResource):
|
||||||
|
properties_schema = {'name': {'Type': 'String'},
|
||||||
|
'value_specs': {'Type': 'Map',
|
||||||
|
'Default': {}},
|
||||||
|
'admin_state_up': {'Type': 'Boolean',
|
||||||
|
'Default': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(Router, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
props = self.prepare_properties(self.properties, self.name)
|
||||||
|
router = self.quantum().create_router({'router': props})['router']
|
||||||
|
self.instance_id_set(router['id'])
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
client.delete_router(self.instance_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def FnGetAtt(self, key):
|
||||||
|
attributes = self.quantum().show_router(
|
||||||
|
self.instance_id)['router']
|
||||||
|
return self.handle_get_attributes(self.name, key, attributes)
|
||||||
|
|
||||||
|
|
||||||
|
class RouterInterface(quantum.QuantumResource):
|
||||||
|
properties_schema = {'router_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'subnet_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(RouterInterface, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
router_id = self.properties.get('router_id')
|
||||||
|
subnet_id = self.properties.get('subnet_id')
|
||||||
|
self.quantum().add_interface_router(router_id,
|
||||||
|
{'subnet_id': subnet_id})
|
||||||
|
self.instance_id_set('%s:%s' % (router_id, subnet_id))
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
(router_id, subnet_id) = self.instance_id.split(':')
|
||||||
|
client.remove_interface_router(router_id,
|
||||||
|
{'subnet_id': subnet_id})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RouterGateway(quantum.QuantumResource):
|
||||||
|
properties_schema = {'router_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'network_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(RouterGateway, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
router_id = self.properties.get('router_id')
|
||||||
|
network_id = self.properties.get('network_id')
|
||||||
|
self.quantum().add_gateway_router(router_id,
|
||||||
|
{'network_id': network_id})
|
||||||
|
self.instance_id_set('%s:%s' % (router_id, network_id))
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
(router_id, network_id) = self.instance_id.split(':')
|
||||||
|
client.remove_gateway_router(router_id)
|
||||||
|
except:
|
||||||
|
pass
|
65
heat/engine/resources/quantum/subnet.py
Normal file
65
heat/engine/resources/quantum/subnet.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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.openstack.common import log as logging
|
||||||
|
from heat.engine.resources.quantum import quantum
|
||||||
|
|
||||||
|
logger = logging.getLogger('heat.engine.quantum')
|
||||||
|
|
||||||
|
|
||||||
|
class Subnet(quantum.QuantumResource):
|
||||||
|
|
||||||
|
allocation_schema = {'start': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'end': {'Type': 'String',
|
||||||
|
'Required': True}}
|
||||||
|
|
||||||
|
properties_schema = {'network_id': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'cidr': {'Type': 'String',
|
||||||
|
'Required': True},
|
||||||
|
'value_specs': {'Type': 'Map',
|
||||||
|
'Default': {}},
|
||||||
|
'name': {'Type': 'String'},
|
||||||
|
'admin_state_up': {'Default': True},
|
||||||
|
'ip_version': {'Type': 'Integer',
|
||||||
|
'AllowedValues': [4, 6],
|
||||||
|
'Default': 4},
|
||||||
|
'gateway_ip': {'Type': 'String'},
|
||||||
|
'allocation_pools': {'Type': 'List',
|
||||||
|
'Schema': allocation_schema}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, name, json_snippet, stack):
|
||||||
|
super(Subnet, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def handle_create(self):
|
||||||
|
props = self.prepare_properties(self.properties, self.name)
|
||||||
|
subnet = self.quantum().create_subnet({'subnet': props})['subnet']
|
||||||
|
self.instance_id_set(subnet['id'])
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
client = self.quantum()
|
||||||
|
try:
|
||||||
|
client.delete_subnet(self.instance_id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def FnGetAtt(self, key):
|
||||||
|
attributes = self.quantum().show_subnet(
|
||||||
|
self.instance_id)['subnet']
|
||||||
|
return self.handle_get_attributes(self.name, key, attributes)
|
@ -29,6 +29,11 @@ from heat.engine.resources import stack
|
|||||||
from heat.engine.resources import user
|
from heat.engine.resources import user
|
||||||
from heat.engine.resources import volume
|
from heat.engine.resources import volume
|
||||||
from heat.engine.resources import wait_condition
|
from heat.engine.resources import wait_condition
|
||||||
|
from heat.engine.resources.quantum import floatingip
|
||||||
|
from heat.engine.resources.quantum import net
|
||||||
|
from heat.engine.resources.quantum import port
|
||||||
|
from heat.engine.resources.quantum import router
|
||||||
|
from heat.engine.resources.quantum import subnet
|
||||||
|
|
||||||
|
|
||||||
_resource_classes = {
|
_resource_classes = {
|
||||||
@ -52,6 +57,14 @@ _resource_classes = {
|
|||||||
'AWS::AutoScaling::AutoScalingGroup': autoscaling.AutoScalingGroup,
|
'AWS::AutoScaling::AutoScalingGroup': autoscaling.AutoScalingGroup,
|
||||||
'AWS::AutoScaling::ScalingPolicy': autoscaling.ScalingPolicy,
|
'AWS::AutoScaling::ScalingPolicy': autoscaling.ScalingPolicy,
|
||||||
'AWS::RDS::DBInstance': dbinstance.DBInstance,
|
'AWS::RDS::DBInstance': dbinstance.DBInstance,
|
||||||
|
'OS::Quantum::FloatingIP': floatingip.FloatingIP,
|
||||||
|
'OS::Quantum::FloatingIPAssociation': floatingip.FloatingIPAssociation,
|
||||||
|
'OS::Quantum::Net': net.Net,
|
||||||
|
'OS::Quantum::Port': port.Port,
|
||||||
|
'OS::Quantum::Router': router.Router,
|
||||||
|
'OS::Quantum::RouterInterface': router.RouterInterface,
|
||||||
|
'OS::Quantum::RouterGateway': router.RouterGateway,
|
||||||
|
'OS::Quantum::Subnet': subnet.Subnet,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +26,13 @@ try:
|
|||||||
swiftclient_present = True
|
swiftclient_present = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
swiftclient_present = False
|
swiftclient_present = False
|
||||||
|
# quantumclient not available in all distributions - make quantum an optional
|
||||||
|
# feature
|
||||||
|
try:
|
||||||
|
from quantumclient.v2_0 import client as quantumclient
|
||||||
|
quantumclient_present = True
|
||||||
|
except ImportError:
|
||||||
|
quantumclient_present = False
|
||||||
|
|
||||||
from heat.common import exception
|
from heat.common import exception
|
||||||
from heat.common import config
|
from heat.common import config
|
||||||
@ -124,6 +131,7 @@ class Resource(object):
|
|||||||
self._nova = {}
|
self._nova = {}
|
||||||
self._keystone = None
|
self._keystone = None
|
||||||
self._swift = None
|
self._swift = None
|
||||||
|
self._quantum = None
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
'''Allow == comparison of two resources'''
|
'''Allow == comparison of two resources'''
|
||||||
@ -281,6 +289,38 @@ class Resource(object):
|
|||||||
self._swift = swiftclient.Connection(**args)
|
self._swift = swiftclient.Connection(**args)
|
||||||
return self._swift
|
return self._swift
|
||||||
|
|
||||||
|
def quantum(self):
|
||||||
|
if quantumclient_present == False:
|
||||||
|
return None
|
||||||
|
if self._quantum:
|
||||||
|
logger.debug('using existing _quantum')
|
||||||
|
return self._quantum
|
||||||
|
|
||||||
|
con = self.context
|
||||||
|
args = {
|
||||||
|
'auth_url': con.auth_url,
|
||||||
|
'service_type': 'network',
|
||||||
|
}
|
||||||
|
|
||||||
|
if con.password is not None:
|
||||||
|
args['username'] = con.username
|
||||||
|
args['password'] = con.password
|
||||||
|
args['tenant_name'] = con.tenant
|
||||||
|
elif con.auth_token is not None:
|
||||||
|
args['username'] = con.service_user
|
||||||
|
args['password'] = con.service_password
|
||||||
|
args['tenant_name'] = con.service_tenant
|
||||||
|
args['token'] = con.auth_token
|
||||||
|
else:
|
||||||
|
logger.error("Quantum connection failed, "
|
||||||
|
"no password or auth_token!")
|
||||||
|
return None
|
||||||
|
logger.debug('quantum args %s', args)
|
||||||
|
|
||||||
|
self._quantum = quantumclient.Client(**args)
|
||||||
|
|
||||||
|
return self._quantum
|
||||||
|
|
||||||
def calculate_properties(self):
|
def calculate_properties(self):
|
||||||
for p, v in self.parsed_template('Properties').items():
|
for p, v in self.parsed_template('Properties').items():
|
||||||
self.properties[p] = v
|
self.properties[p] = v
|
||||||
|
182
heat/tests/test_quantum.py
Normal file
182
heat/tests/test_quantum.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
import nose
|
||||||
|
import unittest
|
||||||
|
import mox
|
||||||
|
import json
|
||||||
|
|
||||||
|
from nose.plugins.attrib import attr
|
||||||
|
|
||||||
|
from heat.common import exception
|
||||||
|
from heat.engine import checkeddict
|
||||||
|
from heat.engine.resources.quantum import net
|
||||||
|
from heat.engine.resources.quantum.quantum import QuantumResource as qr
|
||||||
|
from heat.engine import parser
|
||||||
|
from utils import skip_if
|
||||||
|
|
||||||
|
try:
|
||||||
|
from quantumclient.v2_0 import client as quantumclient
|
||||||
|
except:
|
||||||
|
skip_test = True
|
||||||
|
else:
|
||||||
|
skip_test = False
|
||||||
|
|
||||||
|
|
||||||
|
class FakeQuantum():
|
||||||
|
|
||||||
|
def create_network(self, name):
|
||||||
|
return {"network": {
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"subnets": [],
|
||||||
|
"name": "name",
|
||||||
|
"admin_state_up": False,
|
||||||
|
"shared": False,
|
||||||
|
"tenant_id": "c1210485b2424d48804aad5d39c61b8f",
|
||||||
|
"id": "fc68ea2c-b60b-4b4f-bd82-94ec81110766"
|
||||||
|
}}
|
||||||
|
|
||||||
|
def show_network(self, id):
|
||||||
|
return {"network": {
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"subnets": [],
|
||||||
|
"name": "name",
|
||||||
|
"admin_state_up": False,
|
||||||
|
"shared": False,
|
||||||
|
"tenant_id": "c1210485b2424d48804aad5d39c61b8f",
|
||||||
|
"id": "fc68ea2c-b60b-4b4f-bd82-94ec81110766"
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
@attr(tag=['unit', 'resource'])
|
||||||
|
@attr(speed='fast')
|
||||||
|
class QuantumTest(unittest.TestCase):
|
||||||
|
@skip_if(skip_test, 'unable to import quantumclient')
|
||||||
|
def setUp(self):
|
||||||
|
self.m = mox.Mox()
|
||||||
|
self.m.CreateMock(quantumclient)
|
||||||
|
self.m.StubOutWithMock(net.Net, 'quantum')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.m.UnsetStubs()
|
||||||
|
print "QuantumTest teardown complete"
|
||||||
|
|
||||||
|
def load_template(self):
|
||||||
|
self.path = os.path.dirname(os.path.realpath(__file__)).\
|
||||||
|
replace('heat/tests', 'templates')
|
||||||
|
f = open("%s/Quantum.template" % self.path)
|
||||||
|
t = json.loads(f.read())
|
||||||
|
f.close()
|
||||||
|
return t
|
||||||
|
|
||||||
|
def parse_stack(self, t):
|
||||||
|
class DummyContext():
|
||||||
|
tenant = 'test_tenant'
|
||||||
|
username = 'test_username'
|
||||||
|
password = 'password'
|
||||||
|
auth_url = 'http://localhost:5000/v2.0'
|
||||||
|
stack = parser.Stack(DummyContext(), 'test_stack', parser.Template(t),
|
||||||
|
stack_id=-1, parameters={'external_network': 'abcd1234'})
|
||||||
|
|
||||||
|
return stack
|
||||||
|
|
||||||
|
def create_net(self, t, stack, resource_name):
|
||||||
|
resource = net.Net('test_net',
|
||||||
|
t['Resources'][resource_name],
|
||||||
|
stack)
|
||||||
|
self.assertEqual(None, resource.create())
|
||||||
|
self.assertEqual(net.Net.CREATE_COMPLETE, resource.state)
|
||||||
|
return resource
|
||||||
|
|
||||||
|
def test_validate_properties(self):
|
||||||
|
p = checkeddict.Properties('foo', net.Net.properties_schema)
|
||||||
|
vs = {'router:external': True}
|
||||||
|
p.update({
|
||||||
|
'admin_state_up': False,
|
||||||
|
'value_specs': vs
|
||||||
|
})
|
||||||
|
self.assertEqual(None, qr.validate_properties(p))
|
||||||
|
|
||||||
|
vs['shared'] = True
|
||||||
|
self.assertEqual('shared not allowed in value_specs',
|
||||||
|
qr.validate_properties(p))
|
||||||
|
vs.pop('shared')
|
||||||
|
|
||||||
|
vs['name'] = 'foo'
|
||||||
|
self.assertEqual('name not allowed in value_specs',
|
||||||
|
qr.validate_properties(p))
|
||||||
|
vs.pop('name')
|
||||||
|
|
||||||
|
vs['tenant_id'] = '1234'
|
||||||
|
self.assertEqual('tenant_id not allowed in value_specs',
|
||||||
|
qr.validate_properties(p))
|
||||||
|
vs.pop('tenant_id')
|
||||||
|
|
||||||
|
vs['foo'] = '1234'
|
||||||
|
self.assertEqual(None, qr.validate_properties(p))
|
||||||
|
|
||||||
|
def test_prepare_properties(self):
|
||||||
|
p = checkeddict.Properties('foo', net.Net.properties_schema)
|
||||||
|
p.update({
|
||||||
|
'admin_state_up': False,
|
||||||
|
'value_specs': {'router:external': True}
|
||||||
|
})
|
||||||
|
props = qr.prepare_properties(p, 'resource_name')
|
||||||
|
self.assertEqual({
|
||||||
|
'name': 'resource_name',
|
||||||
|
'router:external': True,
|
||||||
|
'admin_state_up': False
|
||||||
|
}, props)
|
||||||
|
|
||||||
|
@skip_if(skip_test, 'unable to import quantumclient')
|
||||||
|
def test_net(self):
|
||||||
|
fq = FakeQuantum()
|
||||||
|
net.Net.quantum().MultipleTimes().AndReturn(fq)
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
t = self.load_template()
|
||||||
|
stack = self.parse_stack(t)
|
||||||
|
resource = self.create_net(t, stack, 'network')
|
||||||
|
|
||||||
|
resource.validate()
|
||||||
|
|
||||||
|
ref_id = resource.FnGetRefId()
|
||||||
|
self.assertEqual('fc68ea2c-b60b-4b4f-bd82-94ec81110766', ref_id)
|
||||||
|
|
||||||
|
self.assertEqual('ACTIVE', resource.FnGetAtt('status'))
|
||||||
|
try:
|
||||||
|
resource.FnGetAtt('Foo')
|
||||||
|
raise Exception('Expected InvalidTemplateAttribute')
|
||||||
|
except exception.InvalidTemplateAttribute:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
resource.FnGetAtt('id')
|
||||||
|
raise Exception('Expected InvalidTemplateAttribute')
|
||||||
|
except exception.InvalidTemplateAttribute:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertEqual(net.Net.UPDATE_REPLACE, resource.handle_update())
|
||||||
|
|
||||||
|
resource.delete()
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
# allows testing of the test directly, shown below
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv.append(__file__)
|
||||||
|
nose.main()
|
100
templates/Quantum.template
Normal file
100
templates/Quantum.template
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"AWSTemplateFormatVersion" : "2010-09-09",
|
||||||
|
|
||||||
|
"Description" : "Template to test Quantum resources",
|
||||||
|
|
||||||
|
"Parameters" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
"Resources" : {
|
||||||
|
"network": {
|
||||||
|
"Type": "OS::Quantum::Net",
|
||||||
|
"Properties": {
|
||||||
|
"name": "the_network"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unnamed_network": {
|
||||||
|
"Type": "OS::Quantum::Net"
|
||||||
|
},
|
||||||
|
"admin_down_network": {
|
||||||
|
"Type": "OS::Quantum::Net",
|
||||||
|
"Properties": {
|
||||||
|
"admin_state_up": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"subnet": {
|
||||||
|
"Type": "OS::Quantum::Subnet",
|
||||||
|
"Properties": {
|
||||||
|
"network_id": { "Ref" : "network" },
|
||||||
|
"ip_version": 4,
|
||||||
|
"cidr": "10.0.3.0/24",
|
||||||
|
"allocation_pools": [{"start": "10.0.3.20", "end": "10.0.3.150"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"port": {
|
||||||
|
"Type": "OS::Quantum::Port",
|
||||||
|
"Properties": {
|
||||||
|
"device_id": "d6b4d3a5-c700-476f-b609-1493dd9dadc0",
|
||||||
|
"name": "port1",
|
||||||
|
"network_id": { "Ref" : "network" },
|
||||||
|
"fixed_ips": [{
|
||||||
|
"subnet_id": { "Ref" : "subnet" },
|
||||||
|
"ip_address": "10.0.3.21"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"router": {
|
||||||
|
"Type": "OS::Quantum::Router"
|
||||||
|
},
|
||||||
|
|
||||||
|
"router_interface": {
|
||||||
|
"Type": "OS::Quantum::RouterInterface",
|
||||||
|
"Properties": {
|
||||||
|
"router_id": { "Ref" : "router" },
|
||||||
|
"subnet_id": { "Ref" : "subnet" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Outputs" : {
|
||||||
|
"the_network_status" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "network", "status" ]},
|
||||||
|
"Description" : "Status of network"
|
||||||
|
},
|
||||||
|
"port_device_owner" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "port", "device_owner" ]},
|
||||||
|
"Description" : "Device owner of the port"
|
||||||
|
},
|
||||||
|
"port_fixed_ips" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "port", "fixed_ips" ]},
|
||||||
|
"Description" : "Fixed IPs of the port"
|
||||||
|
},
|
||||||
|
"port_mac_address" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "port", "mac_address" ]},
|
||||||
|
"Description" : "MAC address of the port"
|
||||||
|
},
|
||||||
|
"port_status" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "port", "status" ]},
|
||||||
|
"Description" : "Status of the port"
|
||||||
|
},
|
||||||
|
"port_show" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "port", "show" ]},
|
||||||
|
"Description" : "All attributes for port"
|
||||||
|
},
|
||||||
|
"subnet_show" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "subnet", "show" ]},
|
||||||
|
"Description" : "All attributes for subnet"
|
||||||
|
},
|
||||||
|
"network_show" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "network", "show" ]},
|
||||||
|
"Description" : "All attributes for network"
|
||||||
|
},
|
||||||
|
"router_show" : {
|
||||||
|
"Value" : { "Fn::GetAtt" : [ "router", "show" ]},
|
||||||
|
"Description" : "All attributes for router"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,4 +27,4 @@ python-keystoneclient
|
|||||||
glance
|
glance
|
||||||
python-memcached
|
python-memcached
|
||||||
python-swiftclient
|
python-swiftclient
|
||||||
|
python-quantumclient
|
||||||
|
Loading…
Reference in New Issue
Block a user