Merge "Fix v2 requests to non-bleeding edge servers"

This commit is contained in:
Jenkins
2014-09-13 07:36:36 +00:00
committed by Gerrit Code Review
15 changed files with 746 additions and 639 deletions

View File

@@ -21,6 +21,7 @@ import json
import os
import re
import sys
import threading
import uuid
import six
@@ -36,6 +37,8 @@ from glanceclient import exc
from glanceclient.openstack.common import importutils
from glanceclient.openstack.common import strutils
_memoized_property_lock = threading.Lock()
# Decorator for cli-args
def arg(*args, **kwargs):
@@ -367,3 +370,18 @@ def integrity_iter(iter, checksum):
raise IOError(errno.EPIPE,
'Corrupt image download. Checksum was %s expected %s' %
(md5sum, checksum))
def memoized_property(fn):
attr_name = '_lazy_once_' + fn.__name__
@property
def _memoized_property(self):
if hasattr(self, attr_name):
return getattr(self, attr_name)
else:
with _memoized_property_lock:
if not hasattr(self, attr_name):
setattr(self, attr_name, fn(self))
return getattr(self, attr_name)
return _memoized_property

View File

@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import warlock
from glanceclient.common import http
from glanceclient.common import utils
@@ -38,50 +37,21 @@ class Client(object):
self.http_client = http.HTTPClient(utils.strip_version(endpoint),
*args, **kwargs)
self.schemas = schemas.Controller(self.http_client)
image_model = self._get_image_model()
self.images = images.Controller(self.http_client,
image_model)
self.image_tags = image_tags.Controller(self.http_client, image_model)
self.images = images.Controller(self.http_client, self.schemas)
self.image_tags = image_tags.Controller(self.http_client,
self.schemas)
self.image_members = image_members.Controller(self.http_client,
self._get_member_model())
self.schemas)
resource_type_model = self._get_metadefs_resource_type_model()
self.metadefs_resource_type = (
metadefs.ResourceTypeController(self.http_client,
resource_type_model))
metadefs.ResourceTypeController(self.http_client, self.schemas))
property_model = self._get_metadefs_property_model()
self.metadefs_property = (
metadefs.PropertyController(self.http_client, property_model))
metadefs.PropertyController(self.http_client, self.schemas))
object_model = self._get_metadefs_object_model()
self.metadefs_object = (
metadefs.ObjectController(self.http_client, object_model))
metadefs.ObjectController(self.http_client, self.schemas))
namespace_model = self._get_metadefs_namespace_model()
self.metadefs_namespace = (
metadefs.NamespaceController(self.http_client, namespace_model))
def _get_image_model(self):
schema = self.schemas.get('image')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def _get_member_model(self):
schema = self.schemas.get('member')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def _get_metadefs_namespace_model(self):
schema = self.schemas.get('metadefs/namespace')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def _get_metadefs_resource_type_model(self):
schema = self.schemas.get('metadefs/resource_type')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def _get_metadefs_property_model(self):
schema = self.schemas.get('metadefs/property')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def _get_metadefs_object_model(self):
schema = self.schemas.get('metadefs/object')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
metadefs.NamespaceController(self.http_client, self.schemas))

View File

@@ -13,11 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
import warlock
from glanceclient.common import utils
from glanceclient.v2 import schemas
class Controller(object):
def __init__(self, http_client, model):
def __init__(self, http_client, schema_client):
self.http_client = http_client
self.model = model
self.schema_client = schema_client
@utils.memoized_property
def model(self):
schema = self.schema_client.get('member')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def list(self, image_id):
url = '/v2/images/%s/members' % image_id

View File

@@ -13,11 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
import warlock
from glanceclient.common import utils
from glanceclient.v2 import schemas
class Controller(object):
def __init__(self, http_client, model):
def __init__(self, http_client, schema_client):
self.http_client = http_client
self.model = model
self.schema_client = schema_client
@utils.memoized_property
def model(self):
schema = self.schema_client.get('image')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def update(self, image_id, tag_value):
"""

View File

@@ -21,14 +21,20 @@ import warlock
from glanceclient.common import utils
from glanceclient import exc
from glanceclient.openstack.common import strutils
from glanceclient.v2 import schemas
DEFAULT_PAGE_SIZE = 20
class Controller(object):
def __init__(self, http_client, model):
def __init__(self, http_client, schema_client):
self.http_client = http_client
self.model = model
self.schema_client = schema_client
@utils.memoized_property
def model(self):
schema = self.schema_client.get('image')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def list(self, **kwargs):
"""Retrieve a listing of Image objects

View File

@@ -19,14 +19,20 @@ import warlock
from glanceclient.common import utils
from glanceclient.openstack.common import strutils
from glanceclient.v2 import schemas
DEFAULT_PAGE_SIZE = 20
class NamespaceController(object):
def __init__(self, http_client, model):
def __init__(self, http_client, schema_client):
self.http_client = http_client
self.model = model
self.schema_client = schema_client
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/namespace')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def create(self, **kwargs):
"""Create a namespace.
@@ -141,9 +147,14 @@ class NamespaceController(object):
class ResourceTypeController(object):
def __init__(self, http_client, model):
def __init__(self, http_client, schema_client):
self.http_client = http_client
self.model = model
self.schema_client = schema_client
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/resource_type')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def associate(self, namespace, **kwargs):
"""Associate a resource type with a namespace."""
@@ -184,9 +195,14 @@ class ResourceTypeController(object):
class PropertyController(object):
def __init__(self, http_client, model):
def __init__(self, http_client, schema_client):
self.http_client = http_client
self.model = model
self.schema_client = schema_client
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/property')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def create(self, namespace, **kwargs):
"""Create a property.
@@ -259,9 +275,14 @@ class PropertyController(object):
class ObjectController(object):
def __init__(self, http_client, model):
def __init__(self, http_client, schema_client):
self.http_client = http_client
self.model = model
self.schema_client = schema_client
@utils.memoized_property
def model(self):
schema = self.schema_client.get('metadefs/object')
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
def create(self, namespace, **kwargs):
"""Create an object.

View File

@@ -18,6 +18,8 @@ import json
import six
import testtools
from glanceclient.v2.schemas import Schema
class FakeAPI(object):
def __init__(self, fixtures):
@@ -60,6 +62,15 @@ class FakeAPI(object):
return self._request('HEAD', *args, **kwargs)
class FakeSchemaAPI(FakeAPI):
def __init__(cls, *args):
super(FakeSchemaAPI, cls).__init__(*args)
def get(self, *args, **kwargs):
_, raw_schema = self._request('GET', *args, **kwargs)
return Schema(raw_schema)
class RawRequest(object):
def __init__(self, headers, body=None,
version=1.0, status=200, reason="Ok"):

View File

@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from mox3 import mox
import testtools
from glanceclient.v2 import client
@@ -23,21 +22,9 @@ class ClientTest(testtools.TestCase):
def setUp(self):
super(ClientTest, self).setUp()
self.mock = mox.Mox()
self.mock.StubOutWithMock(client.Client, '_get_image_model')
self.mock.StubOutWithMock(client.Client, '_get_member_model')
self.mock.StubOutWithMock(client.Client,
'_get_metadefs_namespace_model')
self.mock.StubOutWithMock(client.Client,
'_get_metadefs_resource_type_model')
self.mock.StubOutWithMock(client.Client,
'_get_metadefs_property_model')
self.mock.StubOutWithMock(client.Client,
'_get_metadefs_object_model')
def tearDown(self):
super(ClientTest, self).tearDown()
self.mock.UnsetStubs()
def test_endpoint(self):
gc = client.Client("http://example.com")

View File

@@ -15,10 +15,9 @@
import errno
import json
import testtools
import six
import warlock
import testtools
from glanceclient import exc
from glanceclient.v2 import images
@@ -39,7 +38,31 @@ _PUBLIC_ID = '857806e7-05b6-48e0-9d40-cb0e6fb727b9'
_SHARED_ID = '331ac905-2a38-44c5-a83d-653db8f08313'
_STATUS_REJECTED_ID = 'f3ea56ff-d7e4-4451-998c-1e3d33539c8e'
fixtures = {
data_fixtures = {
'/v2/schemas/image': {
'GET': (
{},
{
'name': 'image',
'properties': {
'id': {},
'name': {},
'locations': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'metadata': {'type': 'object'},
'url': {'type': 'string'},
},
'required': ['url', 'metadata'],
},
},
},
'additionalProperties': {'type': 'string'}
},
),
},
'/v2/images?limit=%d' % images.DEFAULT_PAGE_SIZE: {
'GET': (
{},
@@ -325,7 +348,11 @@ fixtures = {
}
fake_schema = {
schema_fixtures = {
'image': {
'GET': (
{},
{
'name': 'image',
'properties': {
'id': {},
@@ -339,19 +366,22 @@ fake_schema = {
'url': {'type': 'string'},
},
'required': ['url', 'metadata'],
},
},
}
}
},
'additionalProperties': {'type': 'string'}
}
FakeModel = warlock.model_factory(fake_schema)
)
}
}
class TestController(testtools.TestCase):
def setUp(self):
super(TestController, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.controller = images.Controller(self.api, FakeModel)
self.api = utils.FakeAPI(data_fixtures)
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
self.controller = images.Controller(self.api, self.schema_api)
def test_list_images(self):
# NOTE(bcwaldon):cast to list since the controller returns a generator
@@ -622,8 +652,8 @@ class TestController(testtools.TestCase):
expect_hdrs = {
'Content-Type': 'application/openstack-images-v2.1-json-patch',
}
expect_body = '[{"path": "/barney", "value": "miller", ' \
'"op": "replace"}]'
expect_body = ('[{"path": "/barney", "value": "miller", '
'"op": "replace"}]')
expect = [
('GET', '/v2/images/%s' % image_id, {}, None),
('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body),
@@ -741,7 +771,7 @@ class TestController(testtools.TestCase):
image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8'
new_loc = {'url': 'http://foo.com/', 'metadata': {'spam': 'ham'}}
fixture_idx = '/v2/images/%s' % (image_id)
orig_locations = fixtures[fixture_idx]['GET'][1]['locations']
orig_locations = data_fixtures[fixture_idx]['GET'][1]['locations']
loc_map = dict([(l['url'], l) for l in orig_locations])
loc_map[new_loc['url']] = new_loc
mod_patch = [{'path': '/locations', 'op': 'replace',

View File

@@ -15,8 +15,6 @@
import testtools
import warlock
from glanceclient.v2 import image_members
from tests import utils
@@ -25,7 +23,7 @@ IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1'
MEMBER = '11223344-5566-7788-9911-223344556677'
fixtures = {
data_fixtures = {
'/v2/images/{image}/members'.format(image=IMAGE): {
'GET': (
{},
@@ -58,20 +56,31 @@ fixtures = {
'status': 'accepted'
}
),
},
}
}
fake_schema = {'name': 'member', 'properties': {'image_id': {},
'member_id': {}}}
FakeModel = warlock.model_factory(fake_schema)
schema_fixtures = {
'member': {
'GET': (
{},
{
'name': 'member',
'properties': {
'image_id': {},
'member_id': {}
}
},
)
}
}
class TestController(testtools.TestCase):
def setUp(self):
super(TestController, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.controller = image_members.Controller(self.api, FakeModel)
self.api = utils.FakeAPI(data_fixtures)
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
self.controller = image_members.Controller(self.api, self.schema_api)
def test_list_image_members(self):
image_id = IMAGE

View File

@@ -15,8 +15,6 @@
import testtools
import warlock
from glanceclient.v2 import metadefs
from tests import utils
@@ -59,7 +57,7 @@ def _get_namespace_fixture(ns_name, rt_name=RESOURCE_TYPE1, **kwargs):
return ns
fixtures = {
data_fixtures = {
"/v2/metadefs/namespaces?limit=20": {
"GET": (
{},
@@ -271,10 +269,15 @@ fixtures = {
"updated_at": "2014-08-14T09:07:06Z",
}
),
},
}
}
fake_namespace_schema = {
schema_fixtures = {
"metadefs/namespace":
{
"GET": (
{},
{
"additionalProperties": False,
"definitions": {
"property": {
@@ -299,7 +302,8 @@ fake_namespace_schema = {
},
"default": {},
"minLength": {
"$ref": "#/definitions/positiveIntegerDefault0"
"$ref": "#/definitions/"
"positiveIntegerDefault0"
},
"required": {
"$ref": "#/definitions/stringArray"
@@ -308,7 +312,8 @@ fake_namespace_schema = {
"type": "number"
},
"minItems": {
"$ref": "#/definitions/positiveIntegerDefault0"
"$ref": "#/definitions/"
"positiveIntegerDefault0"
},
"readonly": {
"type": "boolean"
@@ -317,7 +322,8 @@ fake_namespace_schema = {
"type": "number"
},
"maxItems": {
"$ref": "#/definitions/positiveInteger"
"$ref": "#/definitions/"
"positiveInteger"
},
"maxLength": {
"$ref": "#/definitions/positiveInteger"
@@ -395,14 +401,14 @@ fake_namespace_schema = {
"properties": {
"description": {
"type": "string",
"description": "Provides a user friendly description of the "
"namespace.",
"description": "Provides a user friendly description "
"of the namespace.",
"maxLength": 500
},
"updated_at": {
"type": "string",
"description": "Date and time of the last namespace modification "
"(READ-ONLY)",
"description": "Date and time of the last namespace "
"modification (READ-ONLY)",
"format": "date-time"
},
"visibility": {
@@ -463,13 +469,14 @@ fake_namespace_schema = {
},
"display_name": {
"type": "string",
"description": "The user friendly name for the namespace. Used by"
" UI if available.",
"description": "The user friendly name for the "
"namespace. Used by UI if available.",
"maxLength": 80
},
"created_at": {
"type": "string",
"description": "Date and time of namespace creation (READ-ONLY)",
"description": "Date and time of namespace creation "
"(READ-ONLY)",
"format": "date-time"
},
"namespace": {
@@ -479,22 +486,26 @@ fake_namespace_schema = {
},
"protected": {
"type": "boolean",
"description": "If true, namespace will not be deletable."
"description": "If true, namespace will not be "
"deletable."
},
"schema": {
"type": "string"
}
}
}
FakeNamespaceModel = warlock.model_factory(fake_namespace_schema)
),
}
}
class TestNamespaceController(testtools.TestCase):
def setUp(self):
super(TestNamespaceController, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.api = utils.FakeAPI(data_fixtures)
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
self.controller = metadefs.NamespaceController(self.api,
FakeNamespaceModel)
self.schema_api)
def test_list_namespaces(self):
namespaces = list(self.controller.list())

View File

@@ -16,8 +16,6 @@
import six
import testtools
import warlock
from glanceclient.v2 import metadefs
from tests import utils
@@ -50,8 +48,7 @@ def _get_object_fixture(ns_name, obj_name, **kwargs):
"description": "DESCRIPTION",
"maximum": 1000000,
"title": "Quota: CPU Period"
},
},
}},
"schema": "/v2/schemas/metadefs/object",
"created_at": "2014-08-14T09:07:06Z",
"updated_at": "2014-08-14T09:07:06Z",
@@ -61,7 +58,7 @@ def _get_object_fixture(ns_name, obj_name, **kwargs):
return obj
fixtures = {
data_fixtures = {
"/v2/metadefs/namespaces/%s/objects" % NAMESPACE1: {
"GET": (
{},
@@ -98,8 +95,11 @@ fixtures = {
}
}
fake_object_schema = {
schema_fixtures = {
"metadefs/object": {
"GET": (
{},
{
"additionalProperties": False,
"definitions": {
"property": {
@@ -124,7 +124,8 @@ fake_object_schema = {
},
"default": {},
"minLength": {
"$ref": "#/definitions/positiveIntegerDefault0"
"$ref": "#/definitions/positiveInteger"
"Default0"
},
"required": {
"$ref": "#/definitions/stringArray"
@@ -133,7 +134,8 @@ fake_object_schema = {
"type": "number"
},
"minItems": {
"$ref": "#/definitions/positiveIntegerDefault0"
"$ref": "#/definitions/positiveInteger"
"Default0"
},
"readonly": {
"type": "boolean"
@@ -220,7 +222,8 @@ fake_object_schema = {
"properties": {
"created_at": {
"type": "string",
"description": "Date and time of object creation (READ-ONLY)",
"description": "Date and time of object creation "
"(READ-ONLY)",
"format": "date-time"
},
"description": {
@@ -243,21 +246,23 @@ fake_object_schema = {
},
"updated_at": {
"type": "string",
"description": "Date and time of the last object modification "
"(READ-ONLY)",
"description": "Date and time of the last object "
"modification (READ-ONLY)",
"format": "date-time"
},
}
}
FakeObjectModel = warlock.model_factory(fake_object_schema)
)
}
}
class TestObjectController(testtools.TestCase):
def setUp(self):
super(TestObjectController, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.controller = metadefs.ObjectController(self.api,
FakeObjectModel)
self.api = utils.FakeAPI(data_fixtures)
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
self.controller = metadefs.ObjectController(self.api, self.schema_api)
def test_list_object(self):
objects = list(self.controller.list(NAMESPACE1))

View File

@@ -15,8 +15,6 @@
import testtools
import warlock
from glanceclient.v2 import metadefs
from tests import utils
@@ -25,7 +23,7 @@ PROPERTY1 = 'Property1'
PROPERTY2 = 'Property2'
PROPERTYNEW = 'PropertyNew'
fixtures = {
data_fixtures = {
"/v2/metadefs/namespaces/%s/properties" % NAMESPACE1: {
"GET": (
{},
@@ -108,11 +106,14 @@ fixtures = {
{},
{}
)
},
}
}
fake_property_schema = {
schema_fixtures = {
"metadefs/property": {
"GET": (
{},
{
"additionalProperties": False,
"definitions": {
"positiveIntegerDefault0": {
@@ -227,15 +228,18 @@ fake_property_schema = {
}
}
}
FakePropertyModel = warlock.model_factory(fake_property_schema)
)
}
}
class TestPropertyController(testtools.TestCase):
def setUp(self):
super(TestPropertyController, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.api = utils.FakeAPI(data_fixtures)
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
self.controller = metadefs.PropertyController(self.api,
FakePropertyModel)
self.schema_api)
def test_list_property(self):
properties = list(self.controller.list(NAMESPACE1))

View File

@@ -15,8 +15,6 @@
import testtools
import warlock
from glanceclient.v2 import metadefs
from tests import utils
@@ -28,7 +26,7 @@ RESOURCE_TYPE4 = 'ResourceType4'
RESOURCE_TYPENEW = 'ResourceTypeNew'
fixtures = {
data_fixtures = {
"/v2/metadefs/namespaces/%s/resource_types" % NAMESPACE1: {
"GET": (
{},
@@ -84,64 +82,76 @@ fixtures = {
]
}
)
},
}
}
fake_resource_type_schema = {
schema_fixtures = {
"metadefs/resource_type": {
"GET": (
{},
{
"name": "resource_type",
"properties": {
"prefix": {
"type": "string",
"description": "Specifies the prefix to use for the given "
"resource type. Any properties in the namespace "
"should be prefixed with this prefix when being "
"applied to the specified resource type. Must "
"include prefix separator (e.g. a colon :).",
"description": "Specifies the prefix to use for the "
"given resource type. Any properties "
"in the namespace should be prefixed "
"with this prefix when being applied "
"to the specified resource type. Must "
"include prefix separator (e.g. a "
"colon :).",
"maxLength": 80
},
"properties_target": {
"type": "string",
"description": "Some resource types allow more than one "
"key / value pair per instance. For example, "
"Cinder allows user and image metadata on volumes. "
"Only the image properties metadata is evaluated "
"by Nova (scheduling or drivers). This property "
"allows a namespace target to remove the "
"description": "Some resource types allow more than "
"one key / value pair per instance. "
"For example, Cinder allows user and "
"image metadata on volumes. Only the "
"image properties metadata is "
"evaluated by Nova (scheduling or "
"drivers). This property allows a "
"namespace target to remove the "
"ambiguity.",
"maxLength": 80
},
"name": {
"type": "string",
"description": "Resource type names should be aligned with Heat "
"resource types whenever possible: http://docs."
"openstack.org/developer/heat/template_guide/"
"openstack.html",
"description": "Resource type names should be "
"aligned with Heat resource types "
"whenever possible: http://docs."
"openstack.org/developer/heat/"
"template_guide/openstack.html",
"maxLength": 80
},
"created_at": {
"type": "string",
"description": "Date and time of resource type association"
" (READ-ONLY)",
"description": "Date and time of resource type "
"association (READ-ONLY)",
"format": "date-time"
},
"updated_at": {
"type": "string",
"description": "Date and time of the last resource type "
"association modification (READ-ONLY)",
"description": "Date and time of the last resource "
"type association modification "
"(READ-ONLY)",
"format": "date-time"
},
}
}
FakeRTModel = warlock.model_factory(fake_resource_type_schema)
)
}
}
class TestResoureTypeController(testtools.TestCase):
def setUp(self):
super(TestResoureTypeController, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.api = utils.FakeAPI(data_fixtures)
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
self.controller = metadefs.ResourceTypeController(self.api,
FakeRTModel)
self.schema_api)
def test_list_resource_types(self):
resource_types = list(self.controller.list())

View File

@@ -14,7 +14,6 @@
# under the License.
import testtools
import warlock
from glanceclient.v2 import image_tags
from tests import utils
@@ -24,7 +23,7 @@ IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1'
TAG = 'tag01'
fixtures = {
data_fixtures = {
'/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG): {
'DELETE': (
{},
@@ -37,19 +36,25 @@ fixtures = {
'tag_value': TAG
}
),
},
}
}
fake_schema = {'name': 'image', 'properties': {'image_id': {}, 'tags': {}}}
FakeModel = warlock.model_factory(fake_schema)
schema_fixtures = {
'tag': {
'GET': (
{},
{'name': 'image', 'properties': {'image_id': {}, 'tags': {}}}
)
}
}
class TestController(testtools.TestCase):
def setUp(self):
super(TestController, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.controller = image_tags.Controller(self.api, FakeModel)
self.api = utils.FakeAPI(data_fixtures)
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
self.controller = image_tags.Controller(self.api, self.schema_api)
def test_update_image_tag(self):
image_id = IMAGE