Move to python-glanceclient.

This depends on https://review.openstack.org/#/c/6506/ for image
filtering support. Do not merge this until that has been merged.

Change-Id: I12e420f153b7b8323956e741bf9a202e31daa3b5
This commit is contained in:
Gabriel Hurley 2012-04-12 17:13:56 -07:00
parent 5864097f5a
commit 0b94c431ab
16 changed files with 90 additions and 238 deletions

View File

@ -20,123 +20,45 @@
from __future__ import absolute_import
import functools
import logging
import urlparse
from django.utils.decorators import available_attrs
from glanceclient.v1 import client as glance_client
from glance import client as glance_client
from glance.common import exception as glance_exception
from horizon.api.base import APIDictWrapper, url_for
from horizon.api.base import url_for
LOG = logging.getLogger(__name__)
def catch_glance_exception(func):
"""
The glance client sometimes throws ``Exception`` classed exceptions for
HTTP communication issues. Catch those, and rethrow them as
``glance_client.ClientConnectionErrors`` so that we can do something
useful with them.
"""
# TODO(johnp): Remove this once Bug 952618 is fixed in the glance client.
@functools.wraps(func, assigned=available_attrs(func))
def inner_func(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as exc:
exc_message = str(exc)
if('Unknown error occurred' in exc_message or
'Internal Server error' in exc_message):
raise glance_exception.ClientConnectionError(exc_message)
raise
return inner_func
class Image(APIDictWrapper):
"""
Wrapper around glance image dictionary to make it object-like and provide
access to image properties.
"""
_attrs = ['checksum', 'container_format', 'created_at', 'deleted',
'deleted_at', 'disk_format', 'id', 'is_public', 'location',
'name', 'properties', 'size', 'status', 'updated_at', 'owner']
def __getattr__(self, attrname):
if attrname == "properties":
if not hasattr(self, "_properties"):
properties_dict = super(Image, self).__getattr__(attrname)
self._properties = ImageProperties(properties_dict)
return self._properties
else:
return super(Image, self).__getattr__(attrname)
class ImageProperties(APIDictWrapper):
"""
Wrapper around glance image properties dictionary to make it object-like.
"""
_attrs = ['architecture', 'image_location', 'image_state', 'kernel_id',
'project_id', 'ramdisk_id', 'image_type']
@catch_glance_exception
def glanceclient(request):
o = urlparse.urlparse(url_for(request, 'image'))
LOG.debug('glanceclient connection created for host "%s:%d"' %
(o.hostname, o.port))
return glance_client.Client(o.hostname,
o.port,
auth_tok=request.user.token)
url = "://".join((o.scheme, o.netloc))
LOG.debug('glanceclient connection created using token "%s" and url "%s"' %
(request.user.token, url))
return glance_client.Client(endpoint=url, token=request.user.token)
@catch_glance_exception
def image_create(request, image_meta, image_file):
return Image(glanceclient(request).add_image(image_meta, image_file))
@catch_glance_exception
def image_delete(request, image_id):
return glanceclient(request).delete_image(image_id)
return glanceclient(request).images.delete(image_id)
@catch_glance_exception
def image_get(request, image_id):
"""
Returns the actual image file from Glance for image with
supplied identifier
"""
return glanceclient(request).get_image(image_id)[1]
@catch_glance_exception
def image_get_meta(request, image_id):
"""
Returns an Image object populated with metadata for image
with supplied identifier.
"""
return Image(glanceclient(request).get_image_meta(image_id))
return glanceclient(request).images.get(image_id)
@catch_glance_exception
def image_list_detailed(request):
return [Image(i) for i in glanceclient(request).get_images_detailed()]
return glanceclient(request).images.list()
@catch_glance_exception
def image_update(request, image_id, image_meta=None):
image_meta = image_meta and image_meta or {}
return Image(glanceclient(request).update_image(image_id,
image_meta=image_meta))
def image_update(request, image_id, **kwargs):
return glanceclient(request).images.update(image_id, **kwargs)
@catch_glance_exception
def snapshot_list_detailed(request):
filters = {}
filters['property-image_type'] = 'snapshot'
filters['is_public'] = 'none'
return [Image(i) for i in glanceclient(request)
.get_images_detailed(filters=filters)]
filters = {'property-image_type': 'snapshot'}
return glanceclient(request).images.list(filters=filters)

View File

@ -95,12 +95,12 @@ class Server(APIResourceWrapper):
@property
def image_name(self):
from glance.common import exception as glance_exceptions
from glanceclient.common import exceptions as glance_exceptions
from horizon.api import glance
try:
image = glance.image_get_meta(self.request, self.image['id'])
image = glance.image_get(self.request, self.image['id'])
return image.name
except glance_exceptions.NotFound:
except glance_exceptions.ClientException:
return "(not found)"
@property

View File

@ -66,13 +66,15 @@ class UpdateImageForm(forms.SelfHandlingForm):
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
))
public = forms.BooleanField(label=_("Public"),
required=False)
def handle(self, request, data):
# TODO add public flag to image meta properties
image_id = data['image_id']
error_updating = _('Unable to update image "%s".')
meta = {'is_public': True,
meta = {'is_public': data['public'],
'disk_format': data['disk_format'],
'container_format': data['container_format'],
'name': data['name'],
@ -85,7 +87,7 @@ class UpdateImageForm(forms.SelfHandlingForm):
meta['properties']['architecture'] = data['architecture']
try:
api.image_update(request, image_id, meta)
api.image_update(request, image_id, **meta)
messages.success(request, _('Image was successfully updated.'))
except:
exceptions.handle(request, error_updating % image_id)

View File

@ -30,7 +30,7 @@ class OverviewTab(tabs.Tab):
def get_context_data(self, request):
image_id = self.tab_group.kwargs['image_id']
try:
image = api.glance.image_get_meta(self.request, image_id)
image = api.glance.image_get(self.request, image_id)
except:
redirect = reverse('horizon:nova:images_and_snapshots:index')
exceptions.handle(request,

View File

@ -21,7 +21,7 @@
from django import http
from django.core.urlresolvers import reverse
from glance.common import exception as glance_exception
from glanceclient.common import exceptions as glance_exception
from horizon import api
from horizon import test
@ -39,14 +39,14 @@ class ImageViewTests(test.TestCase):
image = self.images.first()
quota_usages = self.quota_usages.first()
self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'image_get')
self.mox.StubOutWithMock(api, 'tenant_quota_usages')
# Two flavor_list calls, however, flavor_list is now memoized.
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
api.image_get_meta(IsA(http.HttpRequest), image.id).AndReturn(image)
api.image_get(IsA(http.HttpRequest), image.id).AndReturn(image)
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_usages)
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
@ -79,7 +79,7 @@ class ImageViewTests(test.TestCase):
volume_choice = "%s:vol" % volume.id
block_device_mapping = {device_name: u"%s::0" % volume_choice}
self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'image_get')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
@ -90,7 +90,7 @@ class ImageViewTests(test.TestCase):
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs.list())
api.security_group_list(IsA(http.HttpRequest)) \
.AndReturn(self.security_groups.list())
api.image_get_meta(IsA(http.HttpRequest), image.id).AndReturn(image)
api.image_get(IsA(http.HttpRequest), image.id).AndReturn(image)
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_create(IsA(http.HttpRequest),
server.name,
@ -124,13 +124,13 @@ class ImageViewTests(test.TestCase):
def test_launch_flavorlist_error(self):
image = self.images.first()
self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'image_get')
self.mox.StubOutWithMock(api, 'tenant_quota_usages')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
api.image_get_meta(IsA(http.HttpRequest),
api.image_get(IsA(http.HttpRequest),
image.id).AndReturn(image)
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(
self.quota_usages.first())
@ -151,13 +151,13 @@ class ImageViewTests(test.TestCase):
def test_launch_keypairlist_error(self):
image = self.images.first()
self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'image_get')
self.mox.StubOutWithMock(api, 'tenant_quota_usages')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
api.image_get_meta(IsA(http.HttpRequest), image.id).AndReturn(image)
api.image_get(IsA(http.HttpRequest), image.id).AndReturn(image)
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(
self.quota_usages.first())
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
@ -183,7 +183,7 @@ class ImageViewTests(test.TestCase):
sec_group = self.security_groups.first()
USER_DATA = 'userData'
self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'image_get')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
self.mox.StubOutWithMock(api, 'security_group_list')
@ -194,7 +194,7 @@ class ImageViewTests(test.TestCase):
api.keypair_list(IgnoreArg()).AndReturn(self.keypairs.list())
api.security_group_list(IsA(http.HttpRequest)) \
.AndReturn(self.security_groups.list())
api.image_get_meta(IgnoreArg(), image.id).AndReturn(image)
api.image_get(IgnoreArg(), image.id).AndReturn(image)
api.volume_list(IgnoreArg()).AndReturn(self.volumes.list())
exc = keystone_exceptions.ClientException('Failed')
api.server_create(IsA(http.HttpRequest),
@ -233,7 +233,7 @@ class ImageViewTests(test.TestCase):
device_name = u'vda'
volume_choice = "%s:vol" % volume.id
self.mox.StubOutWithMock(api, 'image_get_meta')
self.mox.StubOutWithMock(api, 'image_get')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'keypair_list')
@ -247,7 +247,7 @@ class ImageViewTests(test.TestCase):
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs.list())
api.security_group_list(IsA(http.HttpRequest)) \
.AndReturn(self.security_groups.list())
api.image_get_meta(IsA(http.HttpRequest), image.id).AndReturn(image)
api.image_get(IsA(http.HttpRequest), image.id).AndReturn(image)
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
api.tenant_quota_usages(IsA(http.HttpRequest)) \
@ -272,8 +272,8 @@ class ImageViewTests(test.TestCase):
def test_image_detail_get(self):
image = self.images.first()
self.mox.StubOutWithMock(api.glance, 'image_get_meta')
api.glance.image_get_meta(IsA(http.HttpRequest), str(image.id)) \
self.mox.StubOutWithMock(api.glance, 'image_get')
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(self.images.first())
self.mox.ReplayAll()
@ -286,12 +286,12 @@ class ImageViewTests(test.TestCase):
def test_image_detail_get_with_exception(self):
image = self.images.first()
self.mox.StubOutWithMock(api.glance, 'image_get_meta')
api.glance.image_get_meta(IsA(http.HttpRequest), str(image.id)) \
.AndRaise(glance_exception.NotFound)
self.mox.StubOutWithMock(api.glance, 'image_get')
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndRaise(glance_exception.ClientException('Error'))
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:images_and_snapshots:images:detail',
args=[image.id]))
url = reverse('horizon:nova:images_and_snapshots:images:detail',
args=[image.id])
res = self.client.get(url)
self.assertRedirectsNoFollow(res, IMAGES_INDEX_URL)

View File

@ -55,7 +55,7 @@ class LaunchView(forms.ModalFormView):
def get_object(self, *args, **kwargs):
image_id = self.kwargs["image_id"]
try:
self.object = api.image_get_meta(self.request, image_id)
self.object = api.image_get(self.request, image_id)
except:
msg = _('Unable to retrieve image "%s".') % image_id
redirect = reverse('horizon:nova:images_and_snapshots:index')
@ -155,22 +155,26 @@ class UpdateView(forms.ModalFormView):
def get_object(self, *args, **kwargs):
try:
self.object = api.image_get_meta(self.request, kwargs['image_id'])
self.object = api.image_get(self.request, kwargs['image_id'])
except:
msg = _('Unable to retrieve image "%s".') % kwargs['image_id']
msg = _('Unable to retrieve image.')
redirect = reverse('horizon:nova:images_and_snapshots:index')
exceptions.handle(self.request, msg, redirect=redirect)
return self.object
def get_initial(self):
properties = self.object['properties']
properties = self.object.properties
# NOTE(gabriel): glanceclient currently treats "is_public" as a string
# rather than a boolean. This should be fixed in the client.
public = self.object.is_public == "True"
return {'image_id': self.kwargs['image_id'],
'name': self.object.get('name', ''),
'name': self.object.name,
'kernel': properties.get('kernel_id', ''),
'ramdisk': properties.get('ramdisk_id', ''),
'architecture': properties.get('architecture', ''),
'container_format': self.object.get('container_format', ''),
'disk_format': self.object.get('disk_format', ''), }
'container_format': self.object.container_format,
'disk_format': self.object.disk_format,
'public': public}
class DetailView(tabs.TabView):

View File

@ -22,7 +22,7 @@
from copy import deepcopy
from django import http
from django.core.urlresolvers import reverse
from glance.common import exception as glance_exception
from glanceclient.common import exceptions as glance_exception
from mox import IsA
from horizon import api
@ -61,10 +61,10 @@ class ImagesAndSnapshotsTests(test.TestCase):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'nova/images_and_snapshots/index.html')
def test_index_client_conn_error(self):
def test_index_error(self):
self.mox.StubOutWithMock(api, 'image_list_detailed')
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
exc = glance_exception.ClientConnectionError('clientConnError')
exc = glance_exception.ClientException('error')
api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exc)
api.snapshot_list_detailed(IsA(http.HttpRequest)) \
.AndReturn(self.snapshots.list())

View File

@ -46,8 +46,8 @@ class IndexView(tables.MultiTableView):
try:
all_images = api.image_list_detailed(self.request)
images = [im for im in all_images
if im['container_format'] not in ['aki', 'ari'] and
getattr(im.properties, "image_type", '') != "snapshot"]
if im.container_format not in ['aki', 'ari'] and
im.properties.get("image_type", '') != "snapshot"]
except:
images = []
exceptions.handle(self.request, _("Unable to retrieve images."))

View File

@ -25,7 +25,7 @@ from django.conf import settings
from django.contrib import messages
from django.utils.translation import ugettext as _
from cloudfiles import errors as swiftclient
from glance.common import exception as glanceclient
from glanceclient.common import exceptions as glanceclient
from keystoneclient import exceptions as keystoneclient
from novaclient import exceptions as novaclient
@ -128,7 +128,7 @@ UNAUTHORIZED = (keystoneclient.Unauthorized,
novaclient.Unauthorized,
novaclient.Forbidden,
glanceclient.AuthorizationFailure,
glanceclient.NotAuthorized,
glanceclient.Unauthorized,
swiftclient.AuthenticationFailed,
swiftclient.AuthenticationError)
UNAUTHORIZED += tuple(EXCEPTION_CONFIG.get('unauthorized', []))
@ -146,7 +146,7 @@ RECOVERABLE = (keystoneclient.ClientException,
# AuthorizationFailure is raised when Keystone is "unavailable".
keystoneclient.AuthorizationFailure,
novaclient.ClientException,
glanceclient.GlanceException,
glanceclient.ClientException,
swiftclient.Error,
AlreadyExists)
RECOVERABLE += tuple(EXCEPTION_CONFIG.get('recoverable', []))

View File

@ -27,7 +27,7 @@ from django.conf import settings
from django.contrib.messages.storage import default_storage
from django.core.handlers import wsgi
from django.test.client import RequestFactory
from glance import client as glance_client
from glanceclient.v1 import client as glance_client
from keystoneclient.v2_0 import client as keystone_client
from novaclient.v1_1 import client as nova_client
import httplib2
@ -299,7 +299,6 @@ class APITestCase(TestCase):
if not hasattr(self, "glanceclient"):
self.mox.StubOutWithMock(glance_client, 'Client')
self.glanceclient = self.mox.CreateMock(glance_client.Client)
self.glanceclient.token = self.tokens.first().id
return self.glanceclient
def stub_swiftclient(self, expected_calls=1):

View File

@ -18,100 +18,19 @@
# License for the specific language governing permissions and limitations
# under the License.
from glance.common import exception as glance_exception
from horizon import api
from horizon import test
class GlanceApiTests(test.APITestCase):
def test_get_glanceclient(self):
""" Verify the client connection method does what we expect. """
# Replace the original client which is stubbed out in setUp()
api.glance.glanceclient = self._original_glanceclient
client = api.glance.glanceclient(self.request)
self.assertEqual(client.auth_tok, self.tokens.first().id)
def test_image_get_meta(self):
""" Verify "get" returns our custom Image class. """
image = self.images.get(id='1')
glanceclient = self.stub_glanceclient()
glanceclient.get_image_meta(image.id).AndReturn(image)
self.mox.ReplayAll()
ret_val = api.image_get_meta(self.request, image.id)
self.assertIsInstance(ret_val, api.glance.Image)
self.assertEqual(ret_val._apidict, image)
def test_image_list_detailed(self):
""" Verify "list" returns our custom Image class. """
def test_snapshot_list_detailed(self):
images = self.images.list()
filters = {'property-image_type': 'snapshot'}
glanceclient = self.stub_glanceclient()
glanceclient.get_images_detailed().AndReturn(images)
glanceclient.images = self.mox.CreateMockAnything()
glanceclient.images.list(filters=filters).AndReturn(images)
self.mox.ReplayAll()
ret_val = api.image_list_detailed(self.request)
for image in ret_val:
self.assertIsInstance(image, api.glance.Image)
self.assertIn(image._apidict, images)
def test_glance_exception_wrapping_for_internal_server_errors(self):
"""
Verify that generic "Exception" classed exceptions from the glance
client's HTTP Internal Service Errors get converted to
ClientConnectionError's.
"""
# TODO(johnp): Remove once Bug 952618 is fixed in the glance client.
glanceclient = self.stub_glanceclient()
glanceclient.get_images_detailed().AndRaise(
Exception("Internal Server error: "))
self.mox.ReplayAll()
with self.assertRaises(glance_exception.ClientConnectionError):
api.image_list_detailed(self.request)
def test_glance_exception_wrapping_for_generic_http_errors(self):
"""
Verify that generic "Exception" classed exceptions from the glance
client's HTTP errors get converted to ClientConnectionError's.
"""
# TODO(johnp): Remove once Bug 952618 is fixed in the glance client.
glanceclient = self.stub_glanceclient()
glanceclient.get_images_detailed().AndRaise(
Exception("Unknown error occurred! 503 Service Unavailable"))
self.mox.ReplayAll()
with self.assertRaises(glance_exception.ClientConnectionError):
api.image_list_detailed(self.request)
class ImageWrapperTests(test.TestCase):
""" Tests for wrapper classes since they have extra logic attached. """
WITHOUT_PROPERTIES = {'size': 100}
WITH_PROPERTIES = {'properties': {'image_state': 'running'},
'size': 100}
def test_get_properties(self):
image = api.Image(self.WITH_PROPERTIES)
image_props = image.properties
self.assertIsInstance(image_props, api.ImageProperties)
self.assertEqual(image_props.image_state, 'running')
def test_get_other(self):
image = api.Image(self.WITH_PROPERTIES)
self.assertEqual(image.size, 100)
def test_get_properties_missing(self):
image = api.Image(self.WITHOUT_PROPERTIES)
with self.assertRaises(AttributeError):
image.properties
def test_get_other_missing(self):
image = api.Image(self.WITHOUT_PROPERTIES)
with self.assertRaises(AttributeError):
self.assertNotIn('missing', image._attrs,
msg="Test assumption broken. Find new missing attribute")
image.missing
# No assertions are necessary. Verification is handled by mox.
api.glance.snapshot_list_detailed(self.request)

View File

@ -35,9 +35,9 @@ class ServerWrapperTests(test.TestCase):
self.assertEqual(server.id, self.servers.first().id)
def test_image_name(self):
image = api.Image(self.images.first())
self.mox.StubOutWithMock(api.glance, 'image_get_meta')
api.glance.image_get_meta(IsA(http.HttpRequest),
image = self.images.first()
self.mox.StubOutWithMock(api.glance, 'image_get')
api.glance.image_get(IsA(http.HttpRequest),
image.id).AndReturn(image)
self.mox.ReplayAll()

View File

@ -12,7 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from horizon.api import glance
from glanceclient.v1.images import Image, ImageManager
from .utils import TestDataContainer
@ -23,24 +24,21 @@ def data(TEST):
# Snapshots
snapshot_dict = {'name': u'snapshot',
'container_format': u'ami',
'id': 3}
snapshot = glance.Image(snapshot_dict)
snapshot_properties_dict = {'image_type': u'snapshot'}
snapshot.properties = glance.ImageProperties(snapshot_properties_dict)
'id': 3,
'properties': {'image_type': u'snapshot'}}
snapshot = Image(ImageManager(None), snapshot_dict)
TEST.snapshots.add(snapshot)
# Images
image_properties_dict = {'image_type': u'image'}
image_dict = {'id': '1',
'name': 'public_image',
'container_format': 'novaImage'}
public_image = glance.Image(image_dict)
public_image.properties = glance.ImageProperties(image_properties_dict)
'container_format': 'novaImage',
'properties': {'image_type': u'image'}}
public_image = Image(ImageManager(None), image_dict)
image_dict = {'id': '2',
'name': 'private_image',
'container_format': 'aki'}
private_image = glance.Image(image_dict)
private_image.properties = glance.ImageProperties(image_properties_dict)
private_image = Image(ImageManager(None), image_dict)
TEST.images.add(public_image, private_image)

View File

@ -101,6 +101,10 @@ LOGGING = {
'handlers': ['console'],
'propagate': False,
},
'openstack_dashboard': {
'handlers': ['console'],
'propagate': False,
},
'novaclient': {
'handlers': ['console'],
'propagate': False,
@ -109,6 +113,10 @@ LOGGING = {
'handlers': ['console'],
'propagate': False,
},
'glanceclient': {
'handlers': ['console'],
'propagate': False,
},
'nose.plugins.manager': {
'handlers': ['console'],
'propagate': False,

View File

@ -6,7 +6,7 @@ set -o errexit
# Increment me any time the environment should be rebuilt.
# This includes dependncy changes, directory renames, etc.
# Simple integer secuence: 1, 2, 3...
environment_version=15
environment_version=16
#--------------------------------------------------------#
function usage {

View File

@ -6,4 +6,4 @@ python-dateutil
# Horizon Non-pip Requirements
-e git+https://github.com/openstack/python-novaclient.git#egg=python-novaclient
-e git+https://github.com/openstack/python-keystoneclient.git#egg=python-keystoneclient
-e git+https://github.com/openstack/glance.git#egg=glance
-e git+https://github.com/openstack/python-glanceclient.git#egg=python-glanceclient