]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Add a set of native quantum resource types.
authorSteve Baker <sbaker@redhat.com>
Mon, 29 Oct 2012 19:41:13 +0000 (08:41 +1300)
committerSteve Baker <sbaker@redhat.com>
Mon, 5 Nov 2012 03:11:04 +0000 (16:11 +1300)
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

12 files changed:
heat/engine/resources/quantum/__init__.py [new file with mode: 0644]
heat/engine/resources/quantum/floatingip.py [new file with mode: 0644]
heat/engine/resources/quantum/net.py [new file with mode: 0644]
heat/engine/resources/quantum/port.py [new file with mode: 0644]
heat/engine/resources/quantum/quantum.py [new file with mode: 0644]
heat/engine/resources/quantum/router.py [new file with mode: 0644]
heat/engine/resources/quantum/subnet.py [new file with mode: 0644]
heat/engine/resources/register.py
heat/engine/resources/resource.py
heat/tests/test_quantum.py [new file with mode: 0644]
templates/Quantum.template [new file with mode: 0644]
tools/pip-requires

diff --git a/heat/engine/resources/quantum/__init__.py b/heat/engine/resources/quantum/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/heat/engine/resources/quantum/floatingip.py b/heat/engine/resources/quantum/floatingip.py
new file mode 100644 (file)
index 0000000..92d5434
--- /dev/null
@@ -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
diff --git a/heat/engine/resources/quantum/net.py b/heat/engine/resources/quantum/net.py
new file mode 100644 (file)
index 0000000..d625eb4
--- /dev/null
@@ -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)
diff --git a/heat/engine/resources/quantum/port.py b/heat/engine/resources/quantum/port.py
new file mode 100644 (file)
index 0000000..939040f
--- /dev/null
@@ -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)
diff --git a/heat/engine/resources/quantum/quantum.py b/heat/engine/resources/quantum/quantum.py
new file mode 100644 (file)
index 0000000..30e3df4
--- /dev/null
@@ -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)
diff --git a/heat/engine/resources/quantum/router.py b/heat/engine/resources/quantum/router.py
new file mode 100644 (file)
index 0000000..c433687
--- /dev/null
@@ -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
diff --git a/heat/engine/resources/quantum/subnet.py b/heat/engine/resources/quantum/subnet.py
new file mode 100644 (file)
index 0000000..90c6a02
--- /dev/null
@@ -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)
index c626f1f3d8d8f757b161759d106435c26a66ad68..16d6e4eb3872daa258c69a19c08d7d87b67ebf5e 100644 (file)
@@ -29,6 +29,11 @@ from heat.engine.resources import stack
 from heat.engine.resources import user
 from heat.engine.resources import volume
 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 = {
@@ -52,6 +57,14 @@ _resource_classes = {
     'AWS::AutoScaling::AutoScalingGroup': autoscaling.AutoScalingGroup,
     'AWS::AutoScaling::ScalingPolicy': autoscaling.ScalingPolicy,
     '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,
 }
 
 
index 712f42b1ce3416fac7123b3ce523c5a2655c363a..28681ceddab1199fbbefd39f77c01601cfc4d631 100644 (file)
@@ -26,6 +26,13 @@ try:
     swiftclient_present = True
 except ImportError:
     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 config
@@ -124,6 +131,7 @@ class Resource(object):
         self._nova = {}
         self._keystone = None
         self._swift = None
+        self._quantum = None
 
     def __eq__(self, other):
         '''Allow == comparison of two resources'''
@@ -281,6 +289,38 @@ class Resource(object):
         self._swift = swiftclient.Connection(**args)
         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):
         for p, v in self.parsed_template('Properties').items():
             self.properties[p] = v
diff --git a/heat/tests/test_quantum.py b/heat/tests/test_quantum.py
new file mode 100644 (file)
index 0000000..9091cca
--- /dev/null
@@ -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()
diff --git a/templates/Quantum.template b/templates/Quantum.template
new file mode 100644 (file)
index 0000000..1fd3298
--- /dev/null
@@ -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"
+    }
+  }
+}
\ No newline at end of file
index 092419da8d470ef55b7e0c28b08c135d0dd8bed0..9651ae92d92e03b34bfc2888b8122c27e527feb5 100644 (file)
@@ -27,4 +27,4 @@ python-keystoneclient
 glance
 python-memcached
 python-swiftclient
-
+python-quantumclient