diff --git a/apidocs/src/samples/db-flavors-by-id-response-json.txt b/apidocs/src/samples/db-flavors-by-id-response-json.txt index 077e1fb40a..0a1044804f 100644 --- a/apidocs/src/samples/db-flavors-by-id-response-json.txt +++ b/apidocs/src/samples/db-flavors-by-id-response-json.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 199 +Content-Length: 214 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/apidocs/src/samples/db-flavors-by-id-response.json b/apidocs/src/samples/db-flavors-by-id-response.json index c527e76f7b..0fe706822a 100644 --- a/apidocs/src/samples/db-flavors-by-id-response.json +++ b/apidocs/src/samples/db-flavors-by-id-response.json @@ -12,7 +12,7 @@ } ], "name": "m1.tiny", - "ram": 512 + "ram": 512, + "str_id": "1" } } - diff --git a/apidocs/src/samples/db-flavors-response-json.txt b/apidocs/src/samples/db-flavors-response-json.txt index c37475d885..120bc21e90 100644 --- a/apidocs/src/samples/db-flavors-response-json.txt +++ b/apidocs/src/samples/db-flavors-response-json.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 2320 +Content-Length: 2730 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/apidocs/src/samples/db-flavors-response.json b/apidocs/src/samples/db-flavors-response.json index 80598c2a6c..2690a90e49 100644 --- a/apidocs/src/samples/db-flavors-response.json +++ b/apidocs/src/samples/db-flavors-response.json @@ -13,7 +13,8 @@ } ], "name": "m1.tiny", - "ram": 512 + "ram": 512, + "str_id": "1" }, { "id": 2, @@ -28,7 +29,8 @@ } ], "name": "m1.small", - "ram": 2048 + "ram": 2048, + "str_id": "2" }, { "id": 3, @@ -43,7 +45,8 @@ } ], "name": "m1.medium", - "ram": 4096 + "ram": 4096, + "str_id": "3" }, { "id": 4, @@ -58,7 +61,8 @@ } ], "name": "m1.large", - "ram": 8192 + "ram": 8192, + "str_id": "4" }, { "id": 5, @@ -73,7 +77,8 @@ } ], "name": "m1.xlarge", - "ram": 16384 + "ram": 16384, + "str_id": "5" }, { "id": 6, @@ -88,7 +93,8 @@ } ], "name": "m1.nano", - "ram": 64 + "ram": 64, + "str_id": "6" }, { "id": 7, @@ -103,7 +109,8 @@ } ], "name": "m1.micro", - "ram": 128 + "ram": 128, + "str_id": "7" }, { "id": 8, @@ -118,7 +125,8 @@ } ], "name": "m1.rd-smaller", - "ram": 768 + "ram": 768, + "str_id": "8" }, { "id": 9, @@ -133,7 +141,8 @@ } ], "name": "tinier", - "ram": 506 + "ram": 506, + "str_id": "9" }, { "id": 10, @@ -148,7 +157,8 @@ } ], "name": "m1.rd-tiny", - "ram": 512 + "ram": 512, + "str_id": "10" }, { "id": 11, @@ -163,7 +173,8 @@ } ], "name": "eph.rd-tiny", - "ram": 512 + "ram": 512, + "str_id": "11" }, { "id": 12, @@ -178,8 +189,24 @@ } ], "name": "eph.rd-smaller", - "ram": 768 + "ram": 768, + "str_id": "12" + }, + { + "id": null, + "links": [ + { + "href": "https://troveapi.org/v1.0/1234/flavors/custom", + "rel": "self" + }, + { + "href": "https://troveapi.org/flavors/custom", + "rel": "bookmark" + } + ], + "name": "custom.small", + "ram": 512, + "str_id": "custom" } ] } - diff --git a/apidocs/src/samples/db-mgmt-get-host-detail-response.json b/apidocs/src/samples/db-mgmt-get-host-detail-response.json index 54eb2cbf54..f5b50c049e 100644 --- a/apidocs/src/samples/db-mgmt-get-host-detail-response.json +++ b/apidocs/src/samples/db-mgmt-get-host-detail-response.json @@ -10,9 +10,8 @@ } ], "name": "hostname_1", - "percentUsed": 204, - "totalRAM": 2004, + "percentUsed": 12, + "totalRAM": 32000, "usedRAM": 4096 } } - diff --git a/etc/tests/localhost.test.conf b/etc/tests/localhost.test.conf index 701fa95437..9eb3965a8c 100644 --- a/etc/tests/localhost.test.conf +++ b/etc/tests/localhost.test.conf @@ -114,6 +114,12 @@ "name": "eph.rd-smaller", "ram": 768, "local_storage": 2 + }, + { + "id": "custom", + "name": "custom.small", + "ram": 512, + "local_storage": 1 } ], diff --git a/trove/common/apischema.py b/trove/common/apischema.py index c04acc6654..3e2d7fbee9 100644 --- a/trove/common/apischema.py +++ b/trove/common/apischema.py @@ -30,6 +30,13 @@ boolean_string = { "maximum": 1 } +non_empty_string = { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^.*[0-9a-zA-Z]+.*$" +} + configuration_data_types = { "type": "string", "minLength": 1, @@ -51,12 +58,7 @@ configuration_non_empty_string = { flavorref = { 'oneOf': [ - url_ref, - { - "type": "string", - "maxLength": 5, - "pattern": "[0-9]+" - }, + non_empty_string, { "type": "integer" }] @@ -75,13 +77,6 @@ volume_size = { }] } -non_empty_string = { - "type": "string", - "minLength": 1, - "maxLength": 255, - "pattern": "^.*[0-9a-zA-Z]+.*$" -} - host_string = { "type": "string", "minLength": 1, diff --git a/trove/db/sqlalchemy/migrate_repo/versions/035_flavor_id_int_to_string.py b/trove/db/sqlalchemy/migrate_repo/versions/035_flavor_id_int_to_string.py new file mode 100644 index 0000000000..3a40bc8813 --- /dev/null +++ b/trove/db/sqlalchemy/migrate_repo/versions/035_flavor_id_int_to_string.py @@ -0,0 +1,35 @@ +# +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 sqlalchemy.schema import MetaData + +from trove.db.sqlalchemy.migrate_repo.schema import Integer +from trove.db.sqlalchemy.migrate_repo.schema import String +from trove.db.sqlalchemy.migrate_repo.schema import Table + + +meta = MetaData() + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table('instances', meta, autoload=True) + instances.c.flavor_id.alter(String(255)) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table('instances', meta, autoload=True) + instances.c.flavor_id.alter(Integer()) diff --git a/trove/flavor/service.py b/trove/flavor/service.py index 6356bdcaa9..ea5b0d728a 100644 --- a/trove/flavor/service.py +++ b/trove/flavor/service.py @@ -14,6 +14,8 @@ # under the License. +import six + from trove.common import exception from trove.common import wsgi from trove.flavor import models @@ -27,7 +29,7 @@ class FlavorController(wsgi.Controller): """Return a single flavor.""" context = req.environ[wsgi.CONTEXT_KEY] self._validate_flavor_id(id) - flavor = models.Flavor(context=context, flavor_id=int(id)) + flavor = models.Flavor(context=context, flavor_id=id) # Pass in the request to build accurate links. return wsgi.Result(views.FlavorView(flavor, req).data(), 200) @@ -38,6 +40,8 @@ class FlavorController(wsgi.Controller): return wsgi.Result(views.FlavorsView(flavors, req).data(), 200) def _validate_flavor_id(self, id): + if isinstance(id, six.string_types): + return try: if int(id) != float(id): raise exception.NotFound(uuid=id) diff --git a/trove/flavor/views.py b/trove/flavor/views.py index 43265a8f59..943ca84f6f 100644 --- a/trove/flavor/views.py +++ b/trove/flavor/views.py @@ -28,11 +28,19 @@ class FlavorView(object): def data(self): + # If the flavor id cannot be cast to an int, we simply return + # no id and rely on str_id instead. + try: + f_id = int(self.flavor.id) + except ValueError: + f_id = None + flavor = { - 'id': int(self.flavor.id), + 'id': f_id, 'links': self._build_links(), 'name': self.flavor.name, 'ram': self.flavor.ram, + 'str_id': str(self.flavor.id), } if not CONF.trove_volume_support and CONF.device_path is not None: diff --git a/trove/tests/api/flavors.py b/trove/tests/api/flavors.py index bced04658e..f66151b60c 100644 --- a/trove/tests/api/flavors.py +++ b/trove/tests/api/flavors.py @@ -65,8 +65,14 @@ def assert_link_list_is_equal(flavor): assert_true(hasattr(flavor, 'links')) assert_true(flavor.links) + if flavor.id: + flavor_id = str(flavor.id) + else: + flavor_id = flavor.str_id + for link in flavor.links: href = link['href'] + if "self" in link['rel']: expected_href = os.path.join(test_config.dbaas_url, "flavors", str(flavor.id)) @@ -74,12 +80,12 @@ def assert_link_list_is_equal(flavor): msg = ("REL HREF %s doesn't start with %s" % (href, test_config.dbaas_url)) assert_true(href.startswith(url), msg) - url = os.path.join("flavors", str(flavor.id)) - msg = "REL HREF %s doesn't end in 'flavors/id'" % href + url = os.path.join("flavors", flavor_id) + msg = "REL HREF %s doesn't end in '%s'" % (href, url) assert_true(href.endswith(url), msg) elif "bookmark" in link['rel']: base_url = test_config.version_url.replace('http:', 'https:', 1) - expected_href = os.path.join(base_url, "flavors", str(flavor.id)) + expected_href = os.path.join(base_url, "flavors", flavor_id) msg = 'bookmark "href" must be %s, not %s' % (expected_href, href) assert_equal(href, expected_href, msg) else: @@ -139,7 +145,8 @@ class Flavors(object): @test def test_flavor_list_attrs(self): - allowed_attrs = ['id', 'name', 'ram', 'links', 'local_storage'] + allowed_attrs = ['id', 'name', 'ram', 'links', 'local_storage', + 'str_id'] flavors = self.rd_client.flavors.list() attrcheck = AttrCheck() for flavor in flavors: @@ -151,7 +158,8 @@ class Flavors(object): @test def test_flavor_get_attrs(self): - allowed_attrs = ['id', 'name', 'ram', 'links', 'local_storage'] + allowed_attrs = ['id', 'name', 'ram', 'links', 'local_storage', + 'str_id'] flavor = self.rd_client.flavors.get(1) attrcheck = AttrCheck() flavor_dict = flavor._info @@ -163,4 +171,4 @@ class Flavors(object): @test def test_flavor_not_found(self): assert_raises(exceptions.NotFound, - self.rd_client.flavors.get, "detail") + self.rd_client.flavors.get, "foo") diff --git a/trove/tests/api/instances.py b/trove/tests/api/instances.py index b2009ca489..b6c3608db7 100644 --- a/trove/tests/api/instances.py +++ b/trove/tests/api/instances.py @@ -381,6 +381,18 @@ class CreateInstanceFail(object): self.delete_async(result.id) + def test_create_failure_with_empty_flavor(self): + instance_name = "instance-failure-with-empty-flavor" + databases = [] + if VOLUME_SUPPORT: + volume = {'size': 1} + else: + volume = None + assert_raises(exceptions.BadRequest, dbaas.instances.create, + instance_name, '', + volume, databases) + assert_equal(400, dbaas.last_http_code) + @test(enabled=VOLUME_SUPPORT) def test_create_failure_with_empty_volume(self): instance_name = "instance-failure-with-no-volume-size" @@ -694,6 +706,54 @@ class CreateInstance(object): check.volume() +@test(depends_on_classes=[InstanceSetup], + groups=[GROUP, tests.INSTANCES], + runs_after_groups=[tests.PRE_INSTANCES]) +class CreateInstanceFlavors(object): + def _result_is_active(self): + instance = dbaas.instances.get(self.result.id) + if instance.status == "ACTIVE": + return True + else: + # If its not ACTIVE, anything but BUILD must be + # an error. + assert_equal("BUILD", instance.status) + if instance_info.volume is not None: + assert_equal(instance.volume.get('used', None), None) + return False + + def _delete_async(self, instance_id): + dbaas.instances.delete(instance_id) + while True: + try: + dbaas.instances.get(instance_id) + except exceptions.NotFound: + return True + time.sleep(1) + + def _create_with_flavor(self, flavor_id): + if not FAKE: + raise SkipTest("This test only for fake mode.") + instance_name = "instance-with-flavor-%s" % flavor_id + databases = [] + if VOLUME_SUPPORT: + volume = {'size': 1} + else: + volume = None + self.result = dbaas.instances.create(instance_name, flavor_id, volume, + databases) + poll_until(self._result_is_active) + self._delete_async(self.result.id) + + @test + def test_create_with_int_flavor(self): + self._create_with_flavor(1) + + @test + def test_create_with_str_flavor(self): + self._create_with_flavor('custom') + + @test(depends_on_classes=[InstanceSetup], groups=[GROUP_NEUTRON]) class CreateInstanceWithNeutron(unittest.TestCase): @time_out(TIMEOUT_INSTANCE_CREATE) diff --git a/trove/tests/api/mgmt/malformed_json.py b/trove/tests/api/mgmt/malformed_json.py index 4ab6ffee90..44c81935b8 100644 --- a/trove/tests/api/mgmt/malformed_json.py +++ b/trove/tests/api/mgmt/malformed_json.py @@ -131,7 +131,7 @@ class MalformedJson(object): poll_until(_check_instance_status) try: - self.dbaas.instances.resize_instance(self.instance.id, "bad data") + self.dbaas.instances.resize_instance(self.instance.id, "") except Exception as e: resp, body = self.dbaas.client.last_response httpCode = resp.status diff --git a/trove/tests/fakes/nova.py b/trove/tests/fakes/nova.py index 60877a0492..fa38c43b86 100644 --- a/trove/tests/fakes/nova.py +++ b/trove/tests/fakes/nova.py @@ -69,6 +69,7 @@ class FakeFlavors(object): self._add(10, 2, "m1.rd-tiny", 512) self._add(11, 0, "eph.rd-tiny", 512, 1) self._add(12, 20, "eph.rd-smaller", 768, 2) + self._add("custom", 25, "custom.small", 512, 1) # self._add(13, 20, "m1.heat", 512) def _add(self, *args, **kwargs): @@ -76,7 +77,11 @@ class FakeFlavors(object): self.db[new_flavor.id] = new_flavor def get(self, id): - id = int(id) + try: + id = int(id) + except ValueError: + pass + if id not in self.db: raise nova_exceptions.NotFound(404, "Flavor id not found %s" % id) return self.db[id] @@ -617,7 +622,7 @@ class FakeHost(object): """ self.instances = [] self.percentUsed = 0 - self.totalRAM = 2004 # 16384 + self.totalRAM = 32000 # 16384 self.usedRAM = 0 for server in self.servers.list(): print(server) @@ -629,11 +634,11 @@ class FakeHost(object): 'name': server.name, 'status': server.status }) - try: - flavor = FLAVORS.get(server.flavor_ref) - except ValueError: - # Maybe flavor_ref isn't an int? + if (str(server.flavor_ref).startswith('http:') or + str(server.flavor_ref).startswith('https:')): flavor = FLAVORS.get_by_href(server.flavor_ref) + else: + flavor = FLAVORS.get(server.flavor_ref) ram = flavor.ram self.usedRAM += ram decimal = float(self.usedRAM) / float(self.totalRAM) diff --git a/trove/tests/unittests/instance/test_instance_controller.py b/trove/tests/unittests/instance/test_instance_controller.py index e6c5e1a4c8..52612a4195 100644 --- a/trove/tests/unittests/instance/test_instance_controller.py +++ b/trove/tests/unittests/instance/test_instance_controller.py @@ -194,6 +194,12 @@ class TestInstanceController(TestCase): validator = jsonschema.Draft4Validator(schema) self.assertTrue(validator.is_valid(body)) + def test_validate_resize_instance_string(self): + body = {"resize": {"flavorRef": 'foo'}} + schema = self.controller.get_schema('action', body) + validator = jsonschema.Draft4Validator(schema) + self.assertTrue(validator.is_valid(body)) + def test_validate_resize_instance_empty_url(self): body = {"resize": {"flavorRef": ""}} schema = self.controller.get_schema('action', body) @@ -202,10 +208,7 @@ class TestInstanceController(TestCase): errors = sorted(validator.iter_errors(body), key=lambda e: e.path) self.verify_errors(errors[0].context, ["'' is too short", - "'' does not match 'http[s]?://(?:[a-zA-Z]" - "|[0-9]|[$-_@.&+]|[!*\\\\(\\\\),]" - "|(?:%[0-9a-fA-F][0-9a-fA-F]))+'", - "'' does not match '[0-9]+'", + "'' does not match '^.*[0-9a-zA-Z]+.*$'", "'' is not of type 'integer'"], ["flavorRef", "flavorRef", "flavorRef", "flavorRef"],